Implementation

Let's start with a unit test of a Java EE core component. The CarManufacturer boundary executes certain business logic and invokes a CarFactory delegate control:

@Stateless
public class CarManufacturer {
    @Inject
    CarFactory carFactory;

    @PersistenceContext
    EntityManager entityManager;

    public Car manufactureCar(Specification spec) {
        Car car = carFactory.createCar(spec);
        entityManager.merge(car);
        return car;
    }
}

Since the EJB boundary is a plain Java class, it can be instantiated and set up in a unit test. The most commonly used Java unit test technology is JUnit together with Mockito for mocking. The following code snippet shows the car manufacturer test case, instantiating the boundary test object and using Mockito to mock away used delegates:

import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

public class CarManufacturerTest {

    private CarManufacturer testObject;

    @Before
    public void setUp() {
        testObject = new CarManufacturer();
        testObject.carFactory = mock(CarFactory.class);
        testObject.entityManager = mock(EntityManager.class);
    }

    @Test
    public void test() {
        Specification spec = ...
        Car car = ...

        when(testObject.entityManager.merge(any())).then(a -> a.getArgument(0));
        when(testObject.carFactory.createCar(any())).thenReturn(car);

        assertThat(testObject.manufactureCar(spec)).isEqualTo(car);

        verify(testObject.carFactory).createCar(spec);
        verify(testObject.entityManager).merge(car);
    }
}

The JUnit framework instantiates the CarManufacturerTest test class once during the test execution.

The @Before method, setUp() here, is executed every time before a @Test method runs. Similarly, methods annotated with @After run after every test run. The @BeforeClass and @AfterClass methods, however, are only executed once per test class, before and after the execution, respectively.

Mockito methods, such as mock(), when(), or verify() are used to create, set up, and verify mocking behavior, respectively. Mock objects are instructed to behave in a certain way. After the test execution, they can verify whether certain functionality has been called on them.

This is an admittedly easy example, but it contains the essence of unit testing core components. No further custom test runner, neither an embedded container is required to verify the boundary's behavior. As opposed to custom runners, the JUnit framework can run unit tests at a very high rate. Hundreds of examples like these will be executed on modern hardware in no time. The startup time is short and the rest is just Java code execution, with a tiny overhead from the testing framework.

Some readers may have noticed the package-private visibility on the CarManufacturer class. This is due to providing better testability in order to be able to set the delegate on instantiated classes. Test classes that reside in the same package as the boundary are able to modify its dependencies. However, engineers might argue that this violates the encapsulation of the boundary. Theoretically they're right, but no caller will be able to modify the references once the components run in an enterprise container. The referenced object is not the actual delegate, but a proxy thereof, hence the CDI implementation can prevent misuse. It certainly is possible to inject the mock object using reflection or by using constructor-based injection. However, field-based injection together with directly setting the dependencies in the test cases provides better readability with the same production behavior. As of today, many enterprise projects have agreed upon using field dependency injection with package-private visibility.

Another discussion is whether to use custom JUnit runners such as MockitoJUnitRunner together with custom mocking annotations or a plain setup approach, as shown previously. The following code snippet shows a more dense example using a custom runner:

import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class CarManufacturerTest {

    @InjectMocks
    private CarManufacturer testObject;

    @Mock
    private CarFactory carFactory;

    @Mock
    private EntityManager entityManager;

    @Test
    public void test() {
        ...
        when(carFactory.createCar(any())).thenReturn(car);
        ...
        verify(carFactory).createCar(spec);
    }
}

Using the custom Mockito runner allows developers to configure tests with less code as well as to define injections with private visibility in the service class. Using a plain approach, as shown previously, provides more flexibility for complex mock scenarios. However, which method to use in order to run and define Mockito mocks is indeed a question of taste.

Parameterized tests is an additional JUnit functionality to define test cases that are similar in the scenario, but differ in input and output data. The manufactureCar() method could be tested with a variety of input data, resulting in a slightly different outcome. Parameterized test cases enable to develop these scenarios more productively. The following code snippet shows an example of such test cases:

import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class CarManufacturerMassTest {

    private CarManufacturer testObject;

    @Parameterized.Parameter(0)
    public Color chassisColor;

    @Parameterized.Parameter(1)
    public EngineType engineType;

    @Before
    public void setUp() {
        testObject = new CarManufacturer();
        testObject.carFactory = mock(CarFactory.class);
        ...
    }

    @Test
    public void test() {
        // chassisColor & engineType
        ...
    }

    @Parameterized.Parameters(name = "chassis: {0}, engine type: {1}")
    public static Collection<Object[]> testData() {
        return Arrays.asList(
                new Object[]{Color.RED, EngineType.DIESEL, ...},
                new Object[]{Color.BLACK, EngineType.DIESEL, ...}
        );
    }
}

Parameterized test classes are instantiated and executed following the data in the @Parameters test data method. Each element in the returned collection results in a separate test execution. The test class populates its parameter properties and continues the text execution as usual. The test data contains test input parameters as well as expected values.

The benefit of this parameterized approach is that it enables developers to add new test cases by simply adding another line within the testData() method. The preceding example shows the combination of a parameterized unit test using mocks. That combination is only possible using a plain Mockito approach, as described previously, instead of using MockitoJUnitRunner.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.136.233.153