This is an example trying to apply some lessons learned from Uncle Bob's article The little mocker
In this gist first I'll explain what I learned. Then I'll show you what the code does and finally how I tested the code.
When we're making unit test we want to focus on the code we want to test not external dependencies that are not under
our control. To make life easier and being able to test on isolation our code without side effects from the rest
of the world we use test doubles
. Test doubles try to simulate behavior that is in our code but belongs to external agents/objects.
There are diferent types of test doubles
that is the main point of The litlle mocker article. Explain what are those types and when use each one. Lest's explain in one line what are the main test doubles
we can use in our tests:
Dummy is an object that implements the interface of the real object but do nothing. Imagine the class Dog
with the method run
. The real class start running when you do const dog = new Dog(); dog.run();
but dummyDog will do nothing.
Stub Is an object that implements the interface of the real object and returns a result. Imagine an Auth
service with a method isLoggedIn
. You can use a stub that returns true
. This way you forget about authentication and focus on your code.
Spy Is an object that implements the interface of the real object, CAN return a result, but their mission is to check that your code is calling the spied object. Imagine that you want to to know if isLoggedIn
method was called. You could spy Auth
class.
Mock Object: Is a substitute implementation to emulate or instrument other domain code. Mocks are similar to spies but the difference is that you put the assertion into the mock. They are a kind of smart spies. I find it less helpful and to coupled to my taste.
Fakes They are different than the rest of Test Doubles. Fakes implements real bussines behavior.
class AcceptingAuthorizerFake extends Authorizer {
authorize(username, password) {
return expect(username).beEquals('Bob');
}
}
As you can see this fake object returns always true
when username is "Bob". As the author of the article says this can get very complicated.
I agree with the author of the article. stubs
and spies
are usually enough in my daily basics. I've never used mocks
as described in this paper. and I will continue using just stubs and spies but is good to know that we have other tools.
We will explain briefly what the example does before get into the code. The authenticate
method that we want to test is used to log in users into a web page with their Google Account. It uses under ther hood Google Auth 2 flow.
The method only has on parameter. immediate
. If you use it this way inmediate(true)
it will start the oauth 2 dance with Google servers and if you were already log in into your google account and you have permissions in the web page it will authorize to enter into the page. The other way is with immediate
flag set to false
. In that case it will ask you to log into your Google account:
I added the google-auth.js
service and their specs google-auth__spec.js
. The thing is that the method is doing 2 nested callbacks to call Google servers.
First is loading auth2
javascript module:
gapi.load('auth2', () => {
//...
Then in the load
callback it's calling Google Auth servers to authorize the user:
gapi.auth.authorize(params, (response) => {
//...
After those 2 Google calls we have their server response. We need to check if there was an error. In that case we reject
the promise. If there is no error we store on localStorage
the tokens.
What I wanted to accomplish from a test perspective is:
- Ensure that some of the params we pass to Google authorize endpoint do not change by mistake. Imagine someone add a new
scope
. She/he should be aware of it. for that reason we added a spec for it. - I want to make sure that always the server responses with an error the code rejects the promise.
- Always that the server returns valid tokens we store it on localStorage.
Google JS api gapi is loaded into the browser and the code has access to it througth window
object. When we're testing the code we don't want to load that dependency for that reason we do a dummy implementation and assign it to window
object. Then in the code we can spy
over that fake gapi
object and make it return what we need.
I think is too much words for so litle code :) but it has been a good excercise for me to understand better the tools we use. Also is hard to deal with async flows. But when you work with dummy implementations is always easier.