-
Notifications
You must be signed in to change notification settings - Fork 10
Test Doubles
A test double is an object that can stand in for a real object in a test, similar to how a stunt double stands in for an actor in a movie. These are sometimes all commonly referred to as “mocks”, but it's important to distinguish between the different types of test doubles since they all have different uses.
The main reasons for using test doubles are -
- Isolate the code under test
- Speed up test execution
- Make execution deterministic
- Simulate special conditions
- Gain access to hidden information
The most common types of test doubles are -
- stubs
- fakes
- spies
- mocks
A stub has no logic, and only returns what we tell it to return. Stubs can be used when we need an object to return specific values in order to get our code under test into a certain state. While it's usually easy to write stubs by hand, using a mocking framework is often a convenient way to reduce boilerplate.
For example -
Assume we need to a function that logs some message using a logger:
function getUsername(username) {
this.log(username);
// logic to get username
}
Here the actual functionality of the logger is not important to test the functionality of the getUsername() function. So we can just stub the log function of Logger to do nothing during this particular test -
function logStub() {
// do nothing
}
Compared to test stubs, a fake object is a more elaborate variation of a test double. Whereas a test stub can return hard-coded return values and each test may instantiate its own variation to return different values to simulate a different scenario, a fake object is more like an optimized, thinned-down version of the real thing that replicates the behavior of the real thing, but without the side effects and other consequences of using the real thing. A fake doesn’t use a mocking framework: it’s a lightweight implementation of an API that behaves like the real implementation, but isn't suitable for production (e.g. an in-memory database). Fakes can be used when we can't use a real implementation in our test (e.g. if the real implementation is too slow or it talks over the network).
For example -
Assume that a functions connects to a database though a class which has the following functions -
save(user);
findById(id);
These function connects to the actual database, which is unnecessary and time-consuming during actual test. Hence we can use a fake class that has these functions but stores data in an array.
class FakeUserRepository {
constructor() {
this.database = new Array(50);
}
save(user) {
if (findById(user.getId()) === null)
database.push(user);
}
findById(id) {
for (const user of database) {
if (user.getId() === id) return user;
}
return null;
}
}
Here the FakeUserRepository uses an array instead of connecting to the database, which simplifies the test.
A test spy is an object that records its interaction with other objects throughout the code base. When deciding if a test was successful based on the state of available objects alone is not sufficient, we can use test spies and make assertions on things such as the number of calls, arguments passed to specific functions, return values and more.
Test spies are useful to test both callbacks and how certain functions are used throughout the system under test.
For example -
Assume we have a function that performs an asynchronous task and later calls a callback function -
function anAsyncFunction(username, callback) {
setTimeout(function() {
callback(username);
}, 1000);
Here, we can use a spy to investigate whether the callback function has been called with the right parameter.
callbackSpy = createSpy();
anAsyncFunction('TestUser', callbackSpy);
callbackSpy.should.have.been.called.with('TestUser');
A mock has expectations about the way it should be called, and a test should fail if it’s not called that way. Mocks are used to test interactions between objects, and are useful in cases where there are no other visible state changes or return results that you can verify (e.g. if your code reads from disk and you want to ensure that it doesn't do more than one disk read, you can use a mock to verify that the method that does the read is only called once). Mock objects can be much more precise by failing the test as soon as something unexpected happens.
For example -
Assume we want to mock a database though a class which has the following functions -
save(user);
findById(id);
mockDatabase = createMock(Database);
mockDatabase.expects('save').withArgs('TestUser').once();
mockDatabase.expects('findById').withArgs(30).once();
myObj.connectToDatabaseAndSave('TestUser');
myObj.connectToDatabaseAndFindNyId(30);
mockDatabase.verify();
A mock verifies that it executes exaclty as expected. For any other invocation—whether it’s to a different method or to findById() with parameter other than what we’ve told the mock object about—the mock will throw an exception, effectively failing the test. Similarly, the mock will complain if findById() is called more than once and it will complain if an expected interaction never took place.
Sinon is widely used in JavaScript to create test doubles. The three important test doubles of sinon are -
- Spies - Which are same as the spies mentioned above.
- Stubs - Which are same as stubs mentioned above plus they can do everything that a spy can.
- Mocks - Which are same as the mocks mentioned above.
More about sinon is covered in its own section.
- Autolabcli v1.0.0 Docs
- Submission Workflow
- Architecture
- Refactoring Advice
- Feature Development
- Autolabcli Tests
- Events Doc
- Sequence Diagrams
- Testing in Javascript
- Libraries
- Debug Techniques
- Arrow Functions
- Autolabcli v0.1.1 Docs
- References