[Unit Testing] Test Doubles (Stubs, Mocks....etc)
In unit testing, isolation is key. The goal of unit testing is to test individual components, and not an entire system.
The class/object/function you are testing is System Under Test (SUT), and the other components of the system are Collaborators or Dependencies.
An example of this is an in-memory implementation of a database. This fake implementation will not access the actual database, but will use a data structure to store it.
Assume below is what we have - GradeBook will access the database.
We first mock the object, call its method, and then verify it has been called. The mocked object stores all the method that has been called and then verifies it later.
Command Methods don't return anything, but changes the system state.
Eg. void sendReminderEmail(Student student)
For this, we use mocks to verify that the method is called.
Test lifecycle with stubs:
Test lifecycle with mocks:
A mock object simply implements the interface of the object, but doesn't have the definition of the object; meaning that you can detect if a method is triggered, but you don't actually trigger the method.
On the other hand, spies actually spy on real objects. With a spy, you can call the real underlying methods of the object and track every interaction (just as you would with mock).
A spy is considered a partial mock in the sense that it can also stub methods when specified.
The class/object/function you are testing is System Under Test (SUT), and the other components of the system are Collaborators or Dependencies.
Test Double is a generic term for any kind of 'pretend' object used in place of a real object for testing purposes.
There are several types of Testing Doubles. We will start with:
- Fake
- Stub
- Mock
- Command Queries
- Mocks vs Stubs
And then:
- Dummy
- Spies
- Spies vs Mocks
I) Basic Testing Doubles
a) Fake
Fake are objects that have working implementations, but not same as production one. Usually they take some shortcut and have simplified version of production code.Note that Fake is a generic term - that can point to anything; usually mock and stubs.
An example of this is an in-memory implementation of a database. This fake implementation will not access the actual database, but will use a data structure to store it.
b) Stubs
Stubs is an object that holds predefined data and uses it to answer call during tests. It is used when we cannot or don't want to involve objects that would answer with real data or have undesirable side effects.
A stub can replace an object/method in unit testing.
An example is an object that grads some data from the database. We can stub it to prevent it from actually accessing the database.
Below is how we can stub it; using ".thenReturn(...)" to return a pre-defined data, which we can then use to verify.
c) Mock
Mocks are objects that register calls they receive. In test assertion, we can verify on Mocks that all expected actions were performed.
We use mock when we want to verify that a specific code was executed. An example of this can be a functionality that calls email sending service - Note that while we know that a specific code is executed, that specific code is not actually executed.
We don't want to send emails each time we run a test. It's also hard to verify that a right email was sent. So, we will just verify that the email sending service was called.
Below is an example. Testing window.close and door.close might be difficult, but we can verify that these two methods are called.
We first mock the object, call its method, and then verify it has been called. The mocked object stores all the method that has been called and then verifies it later.
d) Command Query Separation
It might still be confusing when to use mock and stub. To simplify, you can think of 2 types of method: Query and Command.
Query Methods return some result and don't change state of system.
Eg. Double averageGrades(Student student) : asks for data, but doesn't change the state.
For Query methods, we use stubs.Command Methods don't return anything, but changes the system state.
Eg. void sendReminderEmail(Student student)
For this, we use mocks to verify that the method is called.
e) Stub vs Mock
Stubs test query methods, Mock tests command methods.
Stubs never fail a unit test, but a Mock can.
Test lifecycle with stubs:
- Setup - Prepare object that is being tested and its stubs collaborators.
- Exercise - Test the functionality.
- Verify state - Use asserts to check object's state.
- Teardown - Clean up resources.
Test lifecycle with mocks:
- Setup data - Prepare object that is being tested.
- Setup expectations - Prepare expectations in mock that is being used by primary object.
- Exercise - Test the functionality.
- Verify expectations - Verify that correct methods has been invoked in mock.
- Verify state - Use asserts to check object's state.
- Teardown - Clean up resources.
II) Dummy and Spies
a) Dummy
Dummy objects are passed around, but never actually used; they are usually used to fill lists.
It's not intended to be used in your tests, and will have no effect on the behavior; they are usually null objects.
An example is passing objects into a list, and getting the length of it.
var TaskManager = function(){
var taskList = [];
return {
addTask: function(task){
taskList.push(task);
},
tasksCount: function(){
return taskList.length;
}
}
}
// Test
var assert = require("assert")
describe('add task', function(){
it('should keep track of the number of tasks', function(){
var DummyTask = function(){ return {} };
var taskManager = new TaskManager();
taskManager.addTask(new DummyTask());
taskManager.addTask(new DummyTask());
assert.equal( taskManager.tasksCount(), 2 );
})
})
b) Spies
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, argument passed to specific functions, return value and more.
Test spies are useful to test both callback and how certain function/methods are used throughout the SUT.
"test should call subscribers on publish": function () {
var callback = sinon.spy();
PubSub.subscribe("message", callback)
PubSub.publishSync("message");
assertTrue(callback.called);
}
c) Spies vs Mock
It seems that spies and mocks are very much the same - both can verify if a method within an object is called - so what's the difference?A mock object simply implements the interface of the object, but doesn't have the definition of the object; meaning that you can detect if a method is triggered, but you don't actually trigger the method.
On the other hand, spies actually spy on real objects. With a spy, you can call the real underlying methods of the object and track every interaction (just as you would with mock).
A spy is considered a partial mock in the sense that it can also stub methods when specified.
III) Resources:
- https://gaboesquivel.com/blog/2014/unit-testing-mocks-stubs-and-spies/(DONE) https://blog.pragmatists.com/test-doubles-fakes-mocks-and-stubs-1a7491dfa3da
- https://www.c-sharpcorner.com/UploadFile/dacca2/understand-stub-mock-and-fake-in-unit-testing/
- https://springframework.guru/mockito-mock-vs-spy-in-spring-boot-tests/
- https://www.martinfowler.com/articles/mocksArentStubs.html
Comments
Post a Comment