Stubbing
A common pattern for unit testing is mocking out functions and data.
Theory
When writing a unit test, you want to run as little code as possible outside the unit that you are testing. Any test on something like a pure function (a function deterministic on its inputs and producing no side effects) will always follow this pattern. However, if you need to test a function or component that has side effects, then you'll want to isolate them from the tests.
You can use a stub to configure how certain functions and data are returned.
Examples
Code under test
Consider this function, which reads a value from a redux store and transforms the data before returning it. Notice the import of the selector
from another file, which is used to read the data.
import { selector } from '../../store';
function getNamesForDisplay() {
const names = selector.users.names;
return names.map(({firstName, lastName}) => `${firstName} ${lastName}`)
}
This function has a side effect because it reads .users.names
from an external store. You could write the unit tests to have code that sets up the store with data and then tests the function. However, this has issues:
- The boilerplate quickly becomes large and distracts from reading the test logic itself.
- The data external to the test will persist between each test case, so reordering tests or changing tests can break other tests. This is bad practice - all unit tests should be completely independent.
To address this, you can use a stub to control the data directly in the test.
Unit test
import { sinon, assert } from '@genesislcap/foundation-testing';
// match the import of the code under test
import { selector } from '../../store';
// import the code under test
import { getNamesForDisplay } from './path-to-function';
const GetNamesForDisplaySuite = suite('getNamesForDisplay()');
GetNamesForDisplaySuite.before.each(() => {
sinon.restore();
});
GetNamesForDisplaySuite('Returns correctly formatted full names from user names array', () => {
const mockData = [
{ firstName: 'John', lastName: 'Doe' },
{ firstName: 'Jane', lastName: 'Smith' }
];
sinon.stub(selectors.users, 'names').returns(mockData);
const result = getNamesForDisplay();
assert.equal(result, ['John Doe', 'Jane Smith']);
});
GetNamesForDisplaySuite('Returns empty array when names array is empty', () => {
sinon.stub(selectors.users, 'names').returns([]);
const result = getNamesForDisplay();
assert.equal(result, []);
});
GetNamesForDisplaySuite.run();
In each test, sinon
is used to stub the data that is returned when the code under test accesses selectors.users.names
. This allows you to control the test cases for the unit test. If any implementations change in other places, this shouldn't change these two unit tests, as they're isolated (in accordance with best practice).
The sinon
import allows other types of stubbing and mocking to be used in your tests. See the Sinon.JS documentation for more information. Conversing with AI chatbots is also a useful way to learn how to write mocks for unit tests.
Best practices
In the above example there is the following code.
GetNamesForDisplaySuite.before.each(() => {
sinon.restore();
});
This ensures that before every test runs, all sinon
mocks are cleared and the code runs as normal.
For our simple example, this isn't strictly necessary. But it is very useful in more complex examples.
Imagine a more complex function under test where you stub many different functions at different times in the same unit test file. As stated earlier, you should ensure that each unit test is completely independent from the others. This means you should not have a unit test that sets up a mock which a later unit test in the file relies on. Resetting all the stubs helps to ensure that doesn't happen, and enables you to quickly remove any stubs so that functions can be used as normal in other test suites.