Introduction
In software engineering, unit testing is crucial to the functionality, maintainability, and reliability of the code base. By isolating the smallest testable unit (a function, class, etc.) and abstracting away its dependencies, it is possible to validate the robustness and accuracy of each unit.
In order to focus on the unit under test itself, we must often mock its dependencies. This way we can simplify our tests and force our unit under test to flow through different logical branches of the code – all without relying on any external systems or potentially changing implementations.
Jest is one of the most common testing frameworks for testing JavaScript Code. TS-Jest provides seamless integration between TypeScript (TS) and Jest, making it simple to write tests in TS. Not only does Jest now integrate with TS, it also has fully fledged mocking functionality that allows us to write high quality unit tests.
In this blog post, we walk through samples of how to utilize mocking in different unit testing scenarios. The code for these samples can also be found in this GitHub repository.
Mocking Instances of Classes
Very often, the class under test depends on instances of other classes. Typically, in following the dependency inversion principle, individual classes should depend on interfaces — the implementation of which are often passed in through the class’s constructor. This gives us much flexibility in testing, allowing us to simply define the mocks that implement the required interfaces and pass them through the constructor of a class when writing its unit tests.
Let’s use the following Introducer
class (from this section of the repo) as an example:
export class Introducer {
constructor(private personClient: IPersonClient) {
}
helloPerson(id: string): string {
return `Hello ${this.personClient.getName(id)}!`;
}
askAQuestion(id: string): string {
const age = this.personClient.getAge(id);
const name = this.personClient.getName(id);
return age > 22 ? `What do you do for a living ${name}?` : `What's your favorite hobby ${name}?`;
}
}
Since this Introducer
class depends on an IPersonClient
instance and because we don’t want to use the real Person API
in our unit tests, we create a mockPersonClient
.
const mockPersonClient = {
getName: mockGetName,
getAge: mockGetAge
};
The underlying IPersonClient
methods are mocked outside of the mockPersonClient
object so that additional assertions can be made around the context of these functions (how many times they were called and with what arguments).
This is done via the mockImplementation
method available on the Jest mock created with the jest.fn()
function:
const mockGetName = jest.fn().mockImplementation((id): string => {
return people[id].name;
});
const mockGetAge = jest.fn().mockImplementation((id): number => {
return people[id].age;
});
expect(mockGetName).toBeCalledTimes(1);
expect(mockGetAge).toBeCalledTimes(1);
Now, at the beginning of each test, we can create an Introducer
instance with our mock IPersonClient
:
const introducer = new Introducer(mockPersonClient);
We should also clear the mocks after (or before) each test is run to make sure there is no carryover of information or state between tests. This is a best practice that should be kept in mind when working with mocks in Jest.
afterEach(()=> {
jest.clearAllMocks();
})
Moreover, with all the mocks in place, we now can write tests for the Introducer
public methods.
The full tests can be found in this test file
Note: To check out the full code/test sample, see the repo
Mocking Static Functions
Like with other dependencies in unit testing, we mock static functions so that we can focus on testing the unit’s logic in isolation. This ensures that the testing process is reliable and efficient. Mocking static functions also allows us to control the behavior and responses of the static function during testing, so we can create deterministic tests that have predictable outcomes.
One common scenario where we may need to mock a static function is when following the Factory Design Pattern to create a new object as follows:
export class PersonFactory {
private static _instance: IPerson;
static getInstance(name: string, age: number): IPerson {
if (!PersonFactory._instance) {
const persona = new Person(name, age);
PersonFactory._instance = persona;
}
return PersonFactory._instance;
}
}
In this case, we want to mock our static getInstance
function within our Factory class.
We use the jest.spyon()
function and the .mockReturnValue()
method to overwrite our original Factory method and return a mocked object.
Let’s take an example, which is detailed further in this section of the repo:
Say, we want to test a method that gets a new Person object from a getInstance
method in a PersonFactory and then returns a message that says hello to that person.
To test this function, as our unit under test, we must first mock our returned person object.
const mockAnnika = {
nickname: 'Annika',
age: 22
};
Then we can use the jest.spyon()
function on the PersonFactory so that the class’s getInstance
method will be overwritten to return our mocked person object.
jest.spyOn(PersonFactory, 'getInstance').mockReturnValue(mockAnnika);
Finally, we can call our function under test and expect the returned message to be a simple hello to our mocked Person.
const returnedHelloAnnika = HelloPerson.sayHelloAnnika();
expect(helloAnnika).toEqual("Hello, " + `${mockAnnika.nickname}!`);
Note: To check out the full code/test sample, see the repo
Mocking with Environment Variables
Environment variables are very useful in software engineering for many reasons including application configuration.
Many classes require these configurations to determine their behavior.
While there is a preference for reading these environment variables in at the app entry point and passing config objects to classes (making testing very simple), there are times when this may not be possible.
In this other case, where you must access the environment variables directly (using process.env.YOUR_ENVIRONMENT_VAR
), there are a few things to consider during testing.
If your tests are simple and every test file can use the same exact set of environment variables, utilize the setupFiles
field of the jest.config.js
to point to a TS file that will be run before each test file is executed (where you can set your environment variables) as shown here.
However, in most cases, where different environment variables or the flexibility to change environment variables on a per test basis is needed, follow the sample scenario described below:
For this scenario, we have a simple (but admittedly useless) LoggingService
that returns different strings based on which environment it is in (Azure vs Local):
export class LoggingService {
constructor() {
}
// returns a string just for example sake
info(message: string): string {
const loggingEnvironment = process.env.LOGGING_ENVIRONMENT;
if (loggingEnvironment == "AZURE") {
return "[AZURE] " + message;
} else {
return "[LOCAL] " + message;
}
}
}
We must unit test to see whether the LoggingService
returns the right string based on the environment variables set.
Because these tests will change environment variables, we first cache the old environment in the beforeAll
block and reset this exact environment after all the tests in the afterAll
block.
Before each test, we importantly utilize the jest.resetModules()
function so that modules are imported again in between each test.
This is especially important if the class being tested captures environment variables globally (in its file but outside the class definition).
Lastly, because the LoggingService
code and what we’re testing is so basic, at the beginning of each test, we simply set the required environment variable and test the output of the info(message: string)
function based on that environment variable:
process.env.LOGGING_ENVIRONMENT = "AZURE";
const message = "message";
expect(loggingService.info(message)).toBe("[AZURE] " + message);
Note: if the entire test file requires the same set of environment variables, you can set them in the
beforeAll
viaprocess.env.______
or add them to atest.env
file and load them using a library like dotenv:
beforeAll(() => {
// cache a copy of user env before tests
cachedEnv = { ...process.env };
// add 'test.env' values to user env
dotenv.config({ override: true, path: path.resolve(__dirname, 'test.env') });
});
beforeEach(() => {
// Resets the module registry - the cache of all required modules.
// This is useful to isolate modules where local state might conflict between tests.
jest.resetModules();
// Create logging service for tests
loggingService = new LoggingService();
})
afterAll(() => {
// reset user env vars
process.env = cachedEnv;
});
test.env
:
LOGGING_ENVIRONMENT=LOCAL
Note: To check out the full code/test sample, see the repo
Mocking NPM Libraries
Sometimes there are instances where you cannot constructor inject a dependency and you rely on constructing a dependency in your code itself.
While this makes testing more difficult, Jest has a workaround.
This section shows how to mock classes from npm packages that are constructed in code.
By using the jest.mock()
method, we can specify the library to mock and add the list of dependencies desired, including the classes that need to be constructed.
As an example, let’s assume we have an HTTP-triggered Typescript Azure Function that deletes entities from Azure Table Storage.
Because TypeScript Azure Functions doesn’t come with a Dependency Injection framework out of the box, in our basic Azure Function we construct a TableClient
inline:
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
...
const tableClient = new TableClient(
config.STORAGE_ENDPOINT,
config.STORAGE_TABLE_NAME,
new AzureNamedKeyCredential(config.STORAGE_ACCOUNT_NAME, config.STORAGE_ACCOUNT_KEY),
);
try {
await tableClient.deleteEntity(entityName, rowKey);
...
} catch {
...
}
...
};
In order to mock this class (as well as the AzureNamedKeyCredential
required in its constructor) from the @azure/data-tables
library, we set up the mock of TableClient
and AzureNamedKeyCredential
at the top of the test file:
const mockedDeleteEntity = jest.fn();
jest.mock('@azure/data-tables', () => ({
// use "function" syntax since they need to be instantiated
AzureNamedKeyCredential: function () {
return 'test credential';
},
TableClient: function () {
return {
deleteEntity: mockedDeleteEntity,
};
},
}));
We can also make assertions on how many times the deleteEntity
function is called on the TableClient
by making its implementation a Jest function (jest.fn()
):
expect(mockedDeleteEntity).toBeCalled();
Note: To check out the full code/test sample, see the repo
Conclusion
Overall, utilizing Jest’s mocking features can make unit testing TypeScript applications easy. In this blog post, we have detailed a few common scenarios that we have come across in our team’s work, but this is certainly not an exhaustive list. Refer to the Jest Mocking Documentation for additional information on mocking with Jest.
Note: Please see the full repo for further details and instructions on how to run these outlined tests.