Mockito Quick Start Guide

Why Mockito?

Mockito is a mocking framework that helps you test particular components of software in isolation. You use Mockito to replace collaborators of the components you’re testing so that methods in each collaborator return desired outputs for given inputs. That way, if an error occurs when testing the component, you know where and why.

Adding Mockito to the Project

First, in order to use Mockito in our project, update the Gradle build script to include JUnit and Mockito:

dependencies {
    testImplementation 'junit:junit:4.13.2'
    
    // mockito-inline is needed instead of mockito-core if you plan to mock final methods or classes, constructors, or static methods
    testImplementation 'org.mockito:mockito-inline:4.11.0'
}

If this project used Maven instead of Gradle, the same two dependencies would be required, just using the Maven XML-based syntax in pom.xml.

A Test Class with Everything

A sample unit test utilizing Mockito looks like the following:

package com.example.catalog.model.pubsub;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.time.ZonedDateTime;

import org.hibernate.Session;
import org.hibernate.Transaction;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import com.example.catalog.exception.CatalogException;
import com.example.catalog.util.CatalogConstants;

@RunWith(MockitoJUnitRunner.class)
public class SubscriptionTest {
    private static final long SUBSCRIBER_ID = 10L;
    private static final long SUBSCRIPTION_ID = 100L;
    private static final long NOTIFICATION_ID = 1000L;
    private static final String MY_FILENAME = "thisIsMyFileName.txt";
    private static final String MY_URL = "http://localhost";

    @Mock
    private Session mockSession;

    @Mock
    private HttpURLConnection mockHttpConnection;

    @Mock
    private Transaction mockTransaction;

    private final Subscriber subscriber = new Subscriber();
    private final Subscription subscription = new Subscription();
    private final ZonedDateTime myDatetime = ZonedDateTime.now();
    private final Notification notification = new Notification(MY_FILENAME, CatalogConstants.Changes.ADDED, myDatetime);

    @Before
    public void setUp() throws IOException {
        subscriber.setId(SUBSCRIBER_ID);
        subscriber.setEmail("abc@qc.com");
        subscriber.setName("Bobby Junior");

        subscription.setId(SUBSCRIPTION_ID);
        subscription.setSubscriber(subscriber);
        subscription.setDatasetName("myAwesomeData.s20250801.e20250802");
        subscription.setRepeatingTimeIntervalString("0 0 * * *");
        subscription.setUrl(MY_URL);
        subscription.setTestMode(true);

        notification.setId(NOTIFICATION_ID);
        notification.setSubscription(subscription);

    lenient().when(mockSession.beginTransaction()).thenReturn(mockTransaction);
        lenient().when(mockHttpConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream());
    }

    @Test
    public void testPublishToSuccess() throws IOException, CatalogException {
        URL mockUrl = spy(URI.create(MY_URL).toURL());
        when(mockUrl.openConnection()).thenReturn(mockHttpConnection);
        when(mockHttpConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_ACCEPTED);

        subscription.setUsername("username");
        subscription.setPassword("password");
        NotificationLog nlog = subscription.publishNotification(notification, mockUrl, mockSession);

        verify(mockHttpConnection).setRequestMethod("POST");
        verify(mockHttpConnection).setDoOutput(true);
        verify(mockHttpConnection).setRequestProperty("Content-Type", "application/json");
        verify(mockHttpConnection)
                .setRequestProperty(eq("Authorization"), startsWith("Basic dXNlcm5hbWU6cGFzc3dvcmQ="));
        verify(mockHttpConnection).getResponseCode();
        verify(mockHttpConnection).disconnect();

        assertEquals(NOTIFICATION_ID, nlog.getNotification().getId());
        assertEquals(SUBSCRIPTION_ID, nlog.getNotification().getSubscription().getId());
        assertEquals(
                SUBSCRIBER_ID,
                nlog.getNotification().getSubscription().getSubscriber().getId());
    }
}

As you can see, we can test the publishNotification by mocking the three collaborators, injecting them into the class under test, and then calling the test method.

Working with the Mockito API

The above example demonstrates the general Mockito test process

  1. Creating stubs to stand in for collaborators
  2. Setting expectations on the stubs to do what you want
  3. Injecting the stubs into the class you plan to test
  4. Testing the methods in the class under test by invoking its methods, which in turn call methods on the stubs
  5. Checking the methods work as expected
  6. Verifying that the methods on the collaborators got invoked the correct number of times, in the correct order

You can use these steps every time you want to use Mockito for replacing collaborators in the class under test, thereby writing true unit tests.

Creating Mocks and Stubs

Mockito has two ways of doing that: using the static mock method or using annotations.

In the example, we added the @Mock annotation to those collaborators that we wanted Mockito to mock:

    @Mock
    private Session mockSession;

    @Mock
    private HttpURLConnection mockHttpConnection;

    @Mock
    private Transaction mockTransaction;

We have three collaborators, so we needed three @Mock annotations on the attributes here.

Setting Expectations

The example mocked the collaborators of Subscription used the methods when and thenReturn to set the expectations on various methods:

when(mockSession.beginTransaction()).thenReturn(mockTransaction);

when(mockHttpConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream());

when(mockUrl.openConnection()).thenReturn(mockHttpConnection);

when(mockHttpConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_ACCEPTED);

The argument to when is the declaration of an invocation of the method you want to call on the stub. The return type connects to the various then methods, like thenReturnthenThrow, or thenAnswer, which are usually chained to the output.

Verifying Method Calls

After returning from when, you can take advantage of one more capability of Mockito: verifying that the methods on the mocks were called the correct number of times, in the correct order:

        verify(mockHttpConnection).setRequestMethod("POST");
        verify(mockHttpConnection).setDoOutput(true);
        verify(mockHttpConnection).setRequestProperty("Content-Type", "application/json");
        verify(mockHttpConnection)
                .setRequestProperty(eq("Authorization"), startsWith("Basic dXNlcm5hbWU6cGFzc3dvcmQ="));
        verify(mockHttpConnection).getResponseCode();
        verify(mockHttpConnection).disconnect();

Running JUnit Tests with Mockito

For JUnit 4, in order to work with the @Mock annotation, add the following @RunWith annotation to your test class:

@RunWith(MockitoJUnitRunner.class)

Alternatively, you can invoke the openMocks method in a setup method tagged with @Before.

Next Generation Java Testing with JUnit 5

What is JUnit 5?

Unlike previous versions of JUnit, JUnit 5 is composed of several different modules from three different sub-projects.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

The JUnit Platform serves as a foundation for launching testing frameworks on the JVM.

JUnit Jupiter is the combination of the new programming model and extension model for writing tests and extensions in JUnit 5.

JUnit Vintage provides a TestEngine for running JUnit 3 and JUnit 4 based tests on the platform.

Why the word “Jupiter”?

Because it starts with “JU”, and it’s the 5th planet from the Sun.

All the packages associated with JUnit 5 have the word “jupiter” in there.

Setup

Use Gradle or Maven

@Test

New package: org.junit.jupiter.api

Other test annotations:

  • @RepeatedTest
  • @ParameterizedTest
  • @TestFactory

Lifecycle Annotations

Each test gets @Test

@BeforeEach, @AfterEach

@BeforeAll, @AfterAll

Disabled tests

@Disabled -> skip a particular test or tests

Method level or class level

Optional parameter to give a reason

Replaces @Ignored in JUnit 4

Test names

Use @DisplayName on class or methods

Supports Unicode and even emojis

Assertions

New methods in JUnit 5

  • assertAll
  • assertThrows, assertDoesNotThrow
  • assertTimeout
  • assertTimeoutPreemptively

Assumptions

Let you test pre-conditions

Static methods in org.junit.jupiter.api.Assumptions

Conditional Execution

Can make tests or test classes conditional, based on:

  • Operating system
  • Java version
  • Some boolean condition
  • Tagging and Filtering

Test Execution Order

By default, test methods will be ordered using an algorithm that is deterministic but intentionally nonobvious. This ensures that subsequent runs of a test suite execute test methods in the same order, thereby allowing for repeatable builds.

To control the order in which test methods are executed, annotate your test class or test interface with @TestMethodOrder and specify the desired MethodOrderer implementation. You can implement your own custom MethodOrderer or use one of the following built-in MethodOrderer implementations.

Test Instance Lifecycle

Nested Test Classes

Use @Nested on non-static inner classes

Nesting can be as deep as you want

Constructor and Method Parameters

In JUnit 4, no parameters in constructors or test methods

Now, parameters can be injected automatically

  • TestInfo
  • TestReporter
  • RepetitionInfo
  • Other custom ParameterResolvers supplied by extensions

Parameterized Tests

Arguably more useful than just repeated

Run a test multiple times with different arguments

@ParameterizedTest

Need at least one source of parameters

NOTE: Stable as of 5.7 (finally!)

Dynamic Tests

Generated at runtime by a factory method

Annotated with @TestFactory

Executing System Command in Cypress Tests

Photo by Darren Halos on Pexels.com

Recently, I wrote a Cypress test where I had to copy a configuration file from one container to another container. As you may know, the easiest way to do that is to use the “docker cp” command. This post is a step-by-step how-to I used to achieve this.

Installing Docker

The “tester” container is based on the official cypress/included:6.3.0 docker image, which in turn is based on the official node:12.18.3-buster docker image. So as the first step, I had to figure out how to install Docker in Debian 10 in order to be able to run “docker cp” from within the container:

FROM cypress/included:6.3.0

RUN apt-get update && apt-get install -y \
  apt-transport-https \
  gnupg2 \
  software-properties-common

RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -

RUN add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian buster stable"

RUN apt-get update && apt-get install -y \
  docker-ce

Creating a Volume for the /var/run/docker.sock

In order to talk to the Docker daemon running outside of the “tester” container, I had to add a volume to mount the famous /var/run/docker.sock in the docker compose file:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock

Executing “docker cp” in Cypress Test

Finally, I was able to execute “docker cp” to copy the configuration file from the “tester” container to the “web_app” container using the Cypress exec command:

const configYamlPath = 'cypress/fixtures/config.yaml';

cy.exec(`docker cp ${configYamlPath} web_app:/opt/web_app`)
  .then(() => cy.reload());

Want to buy me a coffee? Do it here: https://www.buymeacoffee.com/j3rrywan9