Search code examples
databasetypescriptjestjsmockingimplements

Faked database is missing x properties, however those properties are private


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?


Solution

  • 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.

    Playground link to code