I'm trying to write some tests for my code. I'm using dependancy injection, and I'm trying to create a faked version of my database to be used when running tests.
I'm using the keyword implements
to define my faked database, however I'm getting typescript errors due to the fact that this faked DB is missing certain properties, however, those properties are private, and never used outside the class
Here's an example:
class Database {
private client: MongoClient;
public async getData(query: string): Promise<{ name: string }> {
return await this.client.db('db').collection('collection').findOne({ name: query });
}
}
class MockDatabase implements Database {
public async getData(query: string): Promise<{ name: string }> {
return {
name: 'Jo'
}
}
}
function makeApp(database: Database) {
console.log(`Here's your app`);
}
const fakeDB = new MockDatabase();
const app = makeApp(fakeDB)
Typescript will error both when declaring MockDatabase
, as well as when using it in the makeApp
function.
Property 'client' is missing in type 'MockDatabase' but required in type 'Database'
How should I approach faking a database or another service like this?
A Database
needs to have the client
property, and because the property is private
, that means you can only get a valid Database
from the Database
constructor. There is no way to "mock" a Database
with a different declaration, because private
properties need to come from the same declaration in order to be compatible. This restriction is important, because private
properties are not completely inaccessible from outside the object; they are accessible from other instances of the same class. See TypeScript class implements class with private functions for more information.
Anyway, instead of trying to mock a Database
, you should consider creating a new interface
which is just the "public part" of Database
. It would look like this:
// manually written out
interface IDatabase {
getData(query: string): Promise<{ name: string }>
}
You can make the compiler compute this for you, because the keyof
operator only returns the public property names of an object type:
// computed
interface IDatabase extends Pick<Database, keyof Database> { }
The type Pick<Database, keyof Database>
uses the Pick<T, K>
utility type to select just the public properties of Database
. In this case that's just "getData"
, and so the computed IDatabase
is equivalent to the manual one.
And now we change references to Database
to IDatabase
anywhere we only care about the public part:
class MockDatabase implements IDatabase {
public async getData(query: string): Promise<{ name: string }> {
return {
name: 'Jo'
}
}
}
function makeApp(database: IDatabase) {
console.log(`Here's your app`);
}
const fakeDB = new MockDatabase();
const app = makeApp(fakeDB)
And everything works as expected.