Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When to use mock objects in unit tests

I'm aware that there are many questions about mocking and testing, but I didn't find any that helped me perfectly, so I still have problems understanding the follwing:

Please correct me if I got this wrong, but as far as I see, unit tests are used to test the business logic of one particular class in isolation and if there are any objects needed from the outside they will be mocked. So for example if I have a management system for citizens of a simple city that adds citizens to a list and returns the citizen by their names (Assumtion: citizens consist of only a few basic personal information), like this:

public class ProcessClass {

    ArrayList<Citizen> citizenList = new ArrayList<Citizen>();

    public void addCitizen(Citizen citizen) {
        citizenList.add(citizen);
    }

    public Citizen getByName(String name) {
        for (Citizen c : citizenList) {
            if (c.getName().equals(name)) {
                return c;
            }
        }
        return null;
    }

}

If now I want to unit test my ProcessClass do I consider Citizen as an external feature that has to be mocked, or do I simply just create a Citizen for test purposes? If they are mocked, how would I test the method to get the object by its name, since the mock object is not containing the parameters?

like image 431
jle Avatar asked May 10 '19 12:05

jle


People also ask

Should you use mocks in unit tests?

It is unlikely for mocking to be applicable in unit tests, as that means there is a part of the system the unit depends on, making that unit less isolated and less subjected to unit testing. Whenever you reach out to mock things in a unit test that is a good sign you are in fact writing an integration test.

What is the point of mocking tests?

Mocking means creating a fake version of an external or internal service that can stand in for the real one, helping your tests run more quickly and more reliably. When your implementation interacts with an object's properties, rather than its function or behavior, a mock can be used.


5 Answers

More often than not, mocking is used to replace actual calls that are hard to duplicate in testing. For instance, assume ProcessClass makes a REST call to retrieve Citizen information. For a simple unit test, it would be hard to duplicate this REST call. However, you can "mock" the RestTemplate and dictate the different types of returns to insure your code would handle the 200's, 403's, etc. Additionally you could change the type of information to then also test your code to insure that bad data is handled, like missing or null information.

In your case you can actually create a Citizen, and then test that the Citizen is an object in the list or that getByName returns the proper object. So mocking is not needed in this example.

like image 43
JavaJd Avatar answered Oct 27 '22 10:10

JavaJd


In your particular example, no, you would not need to mock anything.

Let's focus on what you would test:

  1. a test where you add and retrieve one Citizen
  2. add 2 Citizens, retrieve one
  3. pass null instead of citizen, and make sure your code doesn't break.
  4. add two citizens with the same name, what would you expect to happen then?
  5. add a citizen without a name.
  6. add a citizen with a null name

etc etc

You can already see a number of different tests you can write.

To make it more interesting you could add some code to your class which exposes a read-only version of your citizenList, then you could check that your list contains exactly the right things.

So, in your scenario you don't need to mock anything as you don't have external dependencies on another system of some kind. Citizen seems to be a simple model class, nothing more.

like image 22
Andrei Dragotoniu Avatar answered Oct 27 '22 09:10

Andrei Dragotoniu


As you're writing new code (along with the new unit tests) or refactoring existing code, you want to be able to run the unit tests over and over to be reasonably confident existing functionality was not broken. Therefore, the unit tests must be stable and fast.

Suppose the class to be tested depends on some external resource such as a database. You make a code change and the unit tests are suddenly failing. Did the unit tests break because of a bug you just introduced, or because the external resource is not available? There is no guarantee the external resource will always be available so the unit tests are unstable. Mock the external resource.

Also, connecting to an external resource can take too much time. When you eventually have thousands of tests which connect to various external resources, the milliseconds to connect to the external resource add up, which slows you down. Mock the external resource.

Now add a CI/CD pipeline. During the build the unit tests fail. Is the external resource down or did your code change break something? Perhaps the build server doesn't have access to an external resource? Mock the external resource.

like image 82
Andrew S Avatar answered Oct 27 '22 11:10

Andrew S


To answer the first part of your question

If now I want to unit test my ProcessClass do I consider Citizen as an external feature that has to be mocked, or do I simply just create a Citizen for test purposes?

Without knowing more about Citizen it is hard to tell. However, the general rule is, that mocking should be done for a reason. Good reasons are:

  • You can not easily make the depended-on-component (DOC) behave as intended for your tests.
  • Does calling the DOC cause any non-derministic behaviour (date/time, randomness, network connections)?
  • The test setup is overly complex and/or maintenance intensive (like, need for external files)
  • The original DOC brings portability problems for your test code.
  • Does using the original DOC cause unnacceptably long build / execution times?
  • Has the DOC stability (maturity) issues that make the tests unreliable, or, worse, is the DOC not even available yet?

For example, you (typically) don't mock standard library math functions like sin or cos, because they don't have any of the abovementioned problems. In your case, you need to judge whether just using Citizen would cause any of the abovementioned problems. If so, it is very likely better to mock it, otherwise you better do not mock.

like image 31
Dirk Herrmann Avatar answered Oct 27 '22 09:10

Dirk Herrmann


If they are mocked, how would I test the method to get the object by its name, since the mock object is not containing the parameters?

You can mock the call to getName, using mockito for example:

Citizen citizen = mock(Citizen.class);
when(citizen.getName()).thenReturn("Bob");

Here is an example of a test for your method

ProcessClass processClass = new ProcessClass();

Citizen citizen1 = mock(Citizen.class);
Citizen citizen2 = mock(Citizen.class);
Citizen citizen3 = mock(Citizen.class);

@Test
public void getByName_shouldReturnCorrectCitizen_whenPresentInList() {
    when(citizen1.getName()).thenReturn("Bob");
    when(citizen2.getName()).thenReturn("Alice");
    when(citizen3.getName()).thenReturn("John");

    processClass.addCitizen(citizen1);
    processClass.addCitizen(citizen2);
    processClass.addCitizen(citizen3);

    Assert.assertEquals(citizen2, processClass.getByName("Alice"));
}

@Test
public void getByName_shouldReturnNull_whenNotPresentInList() {
    when(citizen1.getName()).thenReturn("Bob");

    processClass.addCitizen(citizen1);

    Assert.assertNull(processClass.getByName("Ben"));
}

Note:

I would recommend mocking. Let's say you write 100 tests where you instantiate a Citizen class this way

Citizen c = new Citizen();

and a few months later, your constructor changes to take an argument, which is an object itself, class City for example. Now you have to go back and change all these tests and write:

City city = new City("Paris");
Citizen c = new Citizen(city);

If you mocked Citizen to start with, you wouldn't need to.

Now, as it is POJO and its constructor of the getName method might not change, not mocking should still be ok.

like image 2
Bentaye Avatar answered Oct 27 '22 10:10

Bentaye