In this article we are not going to dwell on such general aspects as testing goals, what testing is, why it is necessary and whether it is necessary at all. These are fundamentals and if you got interested in this very article you should already know the basic things. In fact, it is a topic for a separate tutorial. If you don’t know the basics well enough, we recommend you to improve your knowledge first and then return to this material.

In this article we are going to dive into testing with doubles in unit tests. We would not recommend you to use doubles in integration tests because the latter run the entire scenario workflow without focusing on any details. It’s not that they don’t have to test these details, they just shouldn’t know anything about them at all. Integration tests should be maximally close to customer knowledge about system. The details of this implementation have to be checked on the level of unit tests.

Disclaimer

This article is mostly an author’s point of view and is not an official source. Don’t rely on it blindly but form your own opinion and analyze. If the content of the article looks highly consistent to you and if it is very close to your vision, we recommend you to have a look at the attached links. If you would like to dispute something, feel free to contact the author at shvetsov1988@gmail.com

Testing Classifications

Jay Fields distinguished two kinds of tests in his works.

Testing Classifications
Testing Classifications

A green lozenge in the picture is a unit test in both cases, where the arrow points to the main object for testing (outlined by a green dotted line). Hereafter we will call an object for testing SUT(System Under Test/ Subject Under Test). The SUT also contains information about the connection with collaborators that act as elements in the chain of the overall process.

The whole difference between sociable and solitary tests is that in sociable tests we allow these elements to make real calls. In solitary tests, we substitute these collaborators for doubles.

Doubles: Types of Doubles

As we have already stated, doubles are a common notion for all kinds of fake objects from the perspective of testing. There are a lot of types of doubles but we will speak only about two of them usually applied in our practice. These are stubs and mocks. What is the difference between them?

Let’s look at the screenshot first and try to find mocks and stubs.

Types of Doubles: Mocks and Stubs
Types of Doubles: Mocks and Stubs

This service (UserCreator) saves a user in the system and transfers data via Pub/Sub. Look at #1, 2, 3, 4. What is a mock and what is a stub?

In fact, there are no mocks here. If you assume that it could be option 1 and 2, they aren’t mocks from the ideological point of view. They are closer to Test spies and #3 and #4 are already stubs.

What are Mocks and Stubs & What is the Difference Between Them?

The difference between them is more than just at the level of syntax, they are ideologically different.

There exist two ideologies in testing with doubles.

The first one calls for result verification through state verification. In the example above we block out the AR call (with the help of the allow method) and instead of calling a method we return a state at once which is verified at the end of the test. These are stubs. Put in other words, if there is no method call stubbed before it doesn’t mean that the test goes wrong. We are interested only in verifying returned object. It doesn’t matter from the test point of view how this object was received, via real method call or stubbed method.

Mocks, in their turn, are a part of the ideology of behavior verification. We check whether the method has been called and whether it has been possibly called with definite parameters. That is where the fact of calling a certain method is important for us. No call means that the test has been failed. Actually we don’t need the data this method returns for further processing because it does not confirm the test pass. A mock stops being a mock when it returns anything.

The RSpec syntax helps to separate these two ideologies with the allow and expect methods.

Allow can cut off a method call to return any state and it is not a verifier because a test should be verified by a result, not behavior. Using the allow hooks you choose state verification for the test by default. The expect hooks verify a method call in particular and return an error if there is no call. It is behavior that you test in this case. If expect returns a state it is a reason to think whether you should check if the data for verification returns exactly with this method (seems like you shouldn’t).

You can easily derive a formula from this when to use stubs and mocks.

If you want to use a double on one of your collaborators and the result of collaborator performance should return any state for further verification you need a stub.

If the result of collaborator performance is not important in SUT, you can use a mock. Of course, this collaborator should be covered by a separate unit test if it’s your implementation. If this is a class object of a third-party library, you can give complete control over the test of this method to an integration test.

Reasons for Disliking Testing with Doubles

Duplicating implementation

Duplicating Implementation
Duplicating Implementation

To make a double work we have to put a hook in a specific scenario in advance.

Leading to brittle tests

We decided to change the user search logic and replaced find_by with find. Now our database won’t return anything and the stub will not return too because another method is called. In database testing we wouldn’t need to change anything but here we have to copy the logic again.

Brittle Tests
Brittle Tests

Nested doubles

If for some reason we decide to change the implementation again and the work with a double presupposes an additional call of some methods and parameter transfer, we will need to build nested doubles and copy the implementation again which is not good and clear.

Nested Doubles
Nested Doubles

Not to face the above mentioned issues or to face them consciously you should follow some rules. They appeared from the same source as TDD (test-driven development) which is Java.

Rules for Using Doubles

Mock a role, not an object

Usage

Let’s get back to our service for creating users. We won’t look into the stub for the database so far (we will do it later). There is also a broadcaster on pub/sub among the collaborators of this SUT.

If you’d like to know what happens here, read the article: Monolithic vs. Microservices Architecture: Which is Right for Your App.

The result of its performance doesn’t influence further verification at all. That’s why we don’t need to return any state here and we can easily use a mock to make sure that this method has been called on this collaborator. It will be the main indicator of passing this scenario part. The work of this method can be tested in the unit test of the collaborator.

Mock a Role, not an Object
Mock a Role, not an Object

We’ve created the mock. It’s great but it doesn’t conform to our rule because we’ll have to change the implementation of the UserPublisher class if we decide to work with any other publisher. If it’s a third-party library class, you’ll need to monkey patch it, which is not a way out at all. You can apply such a technique as Dependency Injection that is, in fact, the implementation of the Strategy pattern under the given circumstances.

Dependency Injection
Dependency Injection

Now we can use either option #1 (line 9) or option #2 (line 11). By the way, what is the advantage of using class_double instead of just double in this case? If you don’t know the answer, we’d recommend you to read about verifying doubles. In a nutshell, class_double will return an error if stubbed class don’t provide such a method you are stubbing.

The greatest advantage of this approach is that we are free to pass any object to SUT that will have the implemented publish method with an appropriate signature. This correlates well with the duck typing in Ruby.

Violation

As we have already clarified, violation of this rule can potentially lead to the moment when you will want to change the architecture and face the problem of duplicating implementation and brittle tests.

Mock your own API

Usage

The majority of people understand this rule incorrectly. The point is not in the fact that the owner of external API can completely change the behavior/signature of the used method. The thing is that you might want to use another method if you don’t like the performance of the current one for some reason. It makes no sense to monkey patch it, if there is already an effective alternative option.

Mock Your Own API
Mock Your Own API

For example, the internal execution of the find_by method doesn’t work for us because the API is not ours and we cannot do anything with it except from monkey patching. As there is a better and more correct way just to replace it by another method that works as desired, we actually do it.

If you already doubt that your current code will remain in its original state, don’t use stub in this collaborator.

Violation

As well as in the previous rule, the worst that can happen when you don’t follow this rule is that you may face the problem of duplicating implementation and brittle tests.

Mock peers, not internals

Usage

Mock Peers, not Internals
Mock Peers, not Internals

If to pay attention to the controller you will see a private user_params method. In this case we don’t stub it because this is a part of the inner class execution. The way the needed hash returns for further processing doesn’t indicate the test pass. Method execution itself makes no trouble for us too because such supplemental private methods offer simple processing that won’t require complex logic or undetermined measure of time.

Violation

Violation of the Rule Mock Peers, not Internals
Violation of the Rule Mock Peers, not Internals

If you violate this rule it may happen so that your code won’t work at all (check the screenshot) or will work incorrectly whereby the stub will return the expected result. It is the main reason not to use doubles instead of a real call in this case.

Why Is It Slow? Do We Have Any Influence on It?

Do you have any influence on the execution of this method? What can you do to speed up the execution of this method? Can it possibly be a place for redesign or SRP decomposition? Answer these questions before stubbing the collaborator.

Doubles and Databases

Now, the moment has come when we need to explain why at all it is possible to use doubles instead of the real work with databases.

There are several aspects to consider before applying doubles.

Determinism

Test Your Algorithm, not Flow of Data
Test Your Algorithm, not Flow of Data

While testing a definite algorithm that presupposes working with the database, we initially create and define all scenario conditions under which the algorithm will work.

  • When it’s necessary to verify record creation we should wait until the database creates the record with the parameters we’ve passed to it.
  • When it’s necessary to verify record update we create the record in the database first and then wait until the database returns the updated record.
  • When it’s necessary to verify record deletion we create a record in the database and then wait until the database deletes it.
  • When it’s necessary to verify record return we create a record first and then wait until the database returns it (this example has some exceptions we’ll tell you about below).

All these actions have an execution zone where we have no control over the process and need to wait until it’s restored. In this case the mechanism works within determined conditions every time. If execution of these actions is determinated and we cannot speed it up why don’t we use doubles? Of course, such approach is reasonable if time is crucial and waiting harms tests. In fact it really does. If you can go with 100 tests lasting for half a minute in startups, 7,000 tests that take about forty minutes in real ongoing projects cause great inconveniences for deployment especially when there are existing CI systems there.

If you’d like to make sure that the methods really work as expected, it will be enough to check each of them once in every scenario separately instead of repeating the same stack of actions that execute these methods every time for every new model. Be sure that the profit from such optimization will be huge.

Comfort to use tests

Comfort to Use Tests
Comfort to Use Tests

If you are fine with the current state of tests, there is no point in using doubles. If you avoid tests now (for any reason), you already use them improperly because they should always be there to cover you from wrong execution. Maybe these are not doubles but any other measures that should be taken to speed up the tests. And again, test speed is not what matters most. Don’t apply doubles to every slow call rashly but remember that you should be comfortable with using tests.

Now, we have cleared up the ins and outs. At MLSDev, we consider all of them and that’s why we use doubles for database calls. We don’t like to wait for all determined actions to be executed, especially if to take into account that they aren’t a part of our custom business logic. At the same time there are some exceptions when we don’t apply this approach following the rules of using doubles. All developed practices that are described here allow conducting 7,000-10,000 tests in a few minutes in real ongoing projects.

Let’s speak about particular cases in more detail.

#show know how

Show Know How
Show Know How

We’ve chosen to dwell on #show because the part of this action we are interested in is also a part of #destroy and #update.

Let’s look into it in regards to the rules listed above.

Mock roles, not objects

What is the key concept of this rule? Why should we stub the role in particular?

Business logic of the code can change and we will need to use another object/class. In case of a trivial search by id we can be sure that this business logic won’t require changes even if we integrate more databases in our project. We won’t have significant profit from changing such a construction everywhere.

As for a complex search when we have a selection with some conditions, we don’t recommend to use doubles because the business logic of this part will undergo changes in the future that will deal with the code (modification of the existing one or updating it with additional conditions) and new data storages that will certainly result in changes of the code too.

To sum up, doubles work for #find(id). You can use them in the rest of cases as well but only at your own risk.

Mock your own API

As we have already clarified, #find(id) is the business logic that doesn’t undergo changes. There might be various cases but the general rule says so.

Mock peers, not internals

The construction under review doesn’t violate this rule.

Do we have any influence on its speed?

We’ve already answered this question in the section about peculiarities of doubles and databases. Unfortunately, we cannot avoid unwanted waste of time.

#destroy know how (briefly about #create, #update)

Destroy Know How
Destroy Know How

In this particular case, we won’t analyze the controller because getting user and passing user in the destroyer service is obvious.

It should be mentioned that the rules for #destroy also work for the operations create/update but they require the use of stubs, not mocks.

Mock roles, not objects

In fact, we act within this rule as we stub exactly the role. The argument that works for #find is also valid for #create: to create an object the same construction will be called every time. It will have the same parameter signature (to make sure that there are definite attributes in the model it will be enough to utilize verified doubles). If it’s not enough for you, you can check every successful/unsuccessful scenario for the work of the CRUD methods in use only once. In the rest of cases you can give complete control to doubles not to repeat the same cycle of actions for every new model.

Mock your own API

There will be no changes in #find as well as in the create/destroy/update methods whereas they have only one call signature.

Mock peers, not internals

The construction under review doesn’t violate this rule.

Do we have any influence on its speed?

The same arguments as in the previous example (#show know how) work here too.

Need back-end development for your project?

We have a big team of Ruby on Rails professionals who can deal with a great number of tasks and challenges. They know how to write good code. Contact us to discuss the details.

Get in touch