Tuesday, March 12, 2013

Testing Java classes with field injections

Dependency injection frameworks in Java give us 3 points of injection: constructor, field and setter methods. Many of us prefer field injection because we can skip writing setter methods and keep the number of lines to a minimum. The downside of field injection is that the class no longer can be used or tested without a dependency injection framework, for instance during unit-testing. In this post, I'm going to look at how we can unit-test a class with field injections.

Let's take a look at the class we would like to test:

package com.example.service;
import com.example.domain.Ticket;
import com.example.service.mapper.Mapper;
import com.example.service.remote.RemoteTicketService;
import com.example.service.remote.TicketDto;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class DefaultTicketService implements TicketService {
@Inject
private RemoteTicketService remoteService;
@Inject
private Mapper mapper;
public List<Ticket> findTickets(String artistName) {
List<Ticket> tickets;
try {
List<TicketDto> dtoTicketList = remoteService.ticketSearch(artistName);
tickets = mapper.map(dtoTicketList);
} catch (Exception ex) {
tickets = Collections.emptyList();
}
return tickets;
}
}
package com.example.service;
import com.example.domain.Ticket;
import java.util.List;
public interface TicketService {
List<Ticket> findTickets(String artistName);
}

TicketService is the business facade, and the DefaultTicketService is its implementation. It invokes a remote service and maps response DTO objects to domain objects. Given that we want to test the logic in method findTickets, how do we go about this?

Unit testing with Mockito

Using Mockito 1.9.5 (a mocking framework), we can inject mocks to the class being tested using the @InjectMocks annotation.

package com.example.service;
import com.example.domain.Ticket;
import com.example.service.mapper.Mapper;
import com.example.service.remote.RemoteTicketService;
import com.example.service.remote.TicketDto;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.Before;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.Mockito.*;
import org.mockito.MockitoAnnotations;
public class DefaultTicketServiceTest {
@Mock
private RemoteTicketService mockRemoteTicketService;
@Mock
private Mapper mapper;
@InjectMocks
private TicketService ticketService = new DefaultTicketService();
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testFindTicketsHasResults() {
when(mockRemoteTicketService.ticketSearch(anyString())).thenReturn(Arrays.asList(new TicketDto()));
when(mapper.map(anyListOf(TicketDto.class))).thenReturn(Arrays.asList(new Ticket()));
List<Ticket> tickets = ticketService.findTickets("test");
assertFalse(tickets.isEmpty());
}
@Test
public void testFindTicketsExceptionThrown() {
when(mockRemoteTicketService.ticketSearch(anyString())).thenThrow(Exception.class);
List<Ticket> tickets = ticketService.findTickets("test");
assertTrue(tickets.isEmpty());
}
}

DefaultTicketService class has two field injections, therefore we declare mocks corresponding to these fields. Then we initialize the DefaultTicketService and annotate it with @InjectMocks. Lastly, we call initMocks in our @Before method, causing the mocks declared to be injected into our DefaultTicketService. Since we have references to our mocks, we can control their behavior in test methods depending on what branch of the code we are testing. First test method tests the normal outcome, whereas the second tests exception handling by configuring the mock to throw an exception.

Testing this class with Mockito is focused and simple as it doesn't require a dependency injection framework and only tests logic in our test class. Alternatively, we could have tested this class using a Spring:

package com.example.service;
import com.example.domain.Ticket;
import java.util.List;
import javax.inject.Inject;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"/context.xml", "/test-context.xml"})
public class DefaultTicketServiceTest2 {
@Inject
private TicketService ticketService;
@Test
public void testFindTicketsHasResults() {
List<Ticket> tickets = ticketService.findTickets("test");
assertFalse(tickets.isEmpty());
}
@Test
public void testFindTicketsExceptionThrown() {
List<Ticket> tickets = ticketService.findTickets("exception");
assertTrue(tickets.isEmpty());
}
}

In this case, we have to create a mock implementation of the remote interface and declare it in our test-context.xml. This approach is better suited for integration testing because it will use real beans if corresponding mocks are not declared in the test-context.xml, reducing our control over responses. But, this approach has a bonus, Spring context files are initialized, and therefore tested for consistency.

1 comment:

  1. Additionally, Mockito's Whitebox.setInternalState can be used to set values via reflection.

    ReplyDelete