Unit Testing Using Mocks and Stubs
An introduction to using Mock Objects, and why tools like Mockito are useful.
Join the DZone community and get the full member experience.
Join For FreeIn my last article, Introduction to Unit Testing, I provided an example of how a unit test could be written against a Plain Old Java Object (POJO) using JUnit. This article will focus on mock and stub objects so that no external integrations are required to validate the system under test. In the spirit of the holidays, I will continue using a Candy-inspired set of objects.
Using Mock Objects
Wikipedia defines a mock objects as "simulated objects that mimic the behavior of real objects in controlled ways." Mock objects are important, because they allow the developer to keep the focus on the system (or code) under test and not have to rely on an external element — like a database or persistence layer.
Consider the example below:
public class CandyService {
private static final Double ZERO = new Double("0");
private CandyRepository candyRepository;
public CandyService(CandyRepository candyRepository) {
this.candyRepository = candyRepository;
}
public Double getAverageCandySize() {
Double returnValue = new Double("0");
List<Candy> candyList = candyRepository.getAllCandy();
if (candyList != null && candyList.size() > 0) {
Double candySizeSum = ZERO;
for (Candy thisCandy : candyList) {
if (thisCandy.getSize() != null) {
candySizeSum += thisCandy.getSize();
}
}
if (!candySizeSum.equals(ZERO)) {
returnValue = candySizeSum / candyList.size();
}
}
return returnValue;
}
}
The CandyService class has a single method called getAverageCandySize(), which will return the average size of the candy items in the CandyRepository. A unit test that will be written should validate the logic within the getAverageCandySize() method. However, in order to test the method, a list of Candy objects is expected to be returned from the CandyRepository class.
Using Mockito, as a mocking framework example, it is possible to avoid having to rely on the actual CandyRepository and simluate results in order to facilitate testing. For this example, a simple setup() method is created in the CandyServiceTest class:
@RunWith(MockitoJUnitRunner.class)
public class CandyServiceTest {
private CandyService sut;
@Before
public void setup() throws Exception {
CandyRepository mockCandyRepository = mock(CandyRepository.class);
sut = new CandyService(mockCandyRepository);
when(mockCandyRepository.getAllCandy()).thenReturn(createCandyList());
}
private List<Candy> createCandyList() {
List<Candy> candyList = new ArrayList<Candy>();
Candy candy = new Candy(new Integer("1"), new String("Good & Plenty"), new Integer("51"));
candyList.add(candy);
candy = new Candy(new Integer("2"), new String("Mike & Ike"), new Integer("49"));
candyList.add(candy);
return candyList;
}
}
Within the setup() method, a mock Candy repository (called mockCandyRepository) is established for use during the tests. Mockito uses the when() method to control what results will be found when the getAllCandy() method is called. In this case, the process will include the thenReturn() method and call a private method that will return a simple list of Candy objects.
At this point, the unit test can be added:
@Test
public void standardCandyServiceAverageTest() {
Double result = sut.getAverageCandySize();
assertEquals(result, new Double("50"));
}
In the test, we simply call the getAverageCandySize() method, which will use the setup() method logic to allocate two Candy objects:
Candy object #1, called Good & Plenty, has a size of 51.
Candy object #2, called Mike & Ike, has a size of 49.
Adding the two together yields a size of 100, that when divided by two, lands an average of 50. When running the CandyServiceTest class, the test passes, as expected:
Running with Clover coverage within IntelliJ, illustrates there is 100% coverage for the source class:
As a result of using a mocking framework, like Mockito, we were able to validate the logic within the CandyService without having to rely on an integration point - like a database or persistence layer. This is important, because the data in the database could change or the database could be down - resulting in unexpected behavior when the unit test is executed.
Stub Objects
Stub objects are often confused with mock objects, but serve a slightly different purpose. An example of a stub object that I've encountered often is with an external process, like an email gateway integration point. The system under test may require communicating with an email gateway service.
In fact, the unit tests may not depend on on the email gateway integration point to function, but the connectivity must exist for the class to compile. This is where a stub object can be used to make it appear that the email gateway is functional. Even if the email gateway is called, the stub can be configured to respond back with a generic code - without having to actually send the email message or have an email gateway operational.
Conclusion
ZeroTurnaround's Koskis Kapelonis provides a great differentiator between unit tests and integration tests in his "the correct way to use integration tests in your build process" article. To paraphrase:
If the system under test ...
- uses the database
- uses the network
- uses an external system (e.g. a queue or a mail server)
- reads/writes files or performs other I/O
…then it is an integration test and not a unit test.
It is important for the developer/test writer to keep the focus on the code that is being tested. Using mocks and stubs allows external integration points to be simulated so that the focus can be to validate the program code covered in the test.
In the next article, I plan to talk about how integration tests can be written and when those tests should be executed.
Have a really great day!
Published at DZone with permission of John Vester, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments