Isn't unit testing easier with constructor injection? Talk

Introduction

This is @Autowired, which I absolutely use when writing code in Spring. Recently, it seems that constructor injection is recommended instead of field injection.

Try google to find out why constructor injection is recommended in the first place. There are various reasons, but the one I was most convinced of was "that? Isn't this a unit test effort?", So I wrote about it.

It's annoying to make a mock

Class to be tested

The sample is too good, but if there is a class that sends such an email.

MailService.java


import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;

@RestController
@RequestMapping(path="/mail")
public class MailService {

    @Autowired
    MailSender mailSender;

    @PostMapping
    public void send() {

        SimpleMailMessage msg = new SimpleMailMessage();
        msg.setFrom("[email protected]");
        msg.setTo("[email protected]");
        msg.setSubject("Hello World");
        msg.setText("Welcome to my world.");
       
        mailSender.send(msg);
    }
}

Test code

I will write the test code.

MailServiceTest.java


@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class MailServiceTest {

    @InjectMocks
    MailService mailService;

    @Mock
    MailSender mailSender;

    @Captor
    ArgumentCaptor<SimpleMailMessage> msgCaptor;

    @Test
    public void test() {
        //Executing the method under test
        mailService.send();

        //Among the methods under test, mailSender#send has been executed once
        //Then capture the arguments
        verify(mailSender, times(1)).send(msgCaptor.capture());

        //Validate the contents of the argument
        SimpleMailMessage msg = msgCaptor.getValue();
        assertThat(msg.getFrom(), is("[email protected]"));
        assertNotNull(msg.getTo());
        assertThat(msg.getTo().length, is(1));
        assertThat(msg.getTo()[0], is("[email protected]"));
        assertThat(msg.getSubject(), is("Hello World"));
        assertThat(msg.getText(), is("Welcome to my world."));
    }
}

If it is a field injection class, is it written like this? I hope I'm used to it, but I forget how to write Captor every time, and if I don't comment (even if there is a comment), I can't read it later, and it's annoying in the first place.

Try constructor injection

Class to be tested (only corrections)

MailService.java


public class MailService {

    private final MailSender mailSender;

    @Autowired
    public MailService(MailSender mailSender) {
        this.mailSender = mailSender;
    }

    //The following is omitted

Test code

MailServiceTest.java


public class MailServiceTest {

    /**
     *Create your own mailSender mock class
     */
    private class MailSenderMock implements MailSender {
        SimpleMailMessage simpleMessage;
        @Override
        public void send(SimpleMailMessage simpleMessage) throws MailException {
            this.simpleMessage = simpleMessage;
        }
        @Override
        public void send(SimpleMailMessage... simpleMessages) throws MailException {
        }
    }

    @Test
    public void test() {

        //Pass the mock class to create an instance
        MailSenderMock mailSenderMock = new MailSenderMock();
        MailService mailService = new MailService(mailSenderMock);

        //Executing the method under test
        mailService.send();

        //Verification
        SimpleMailMessage msg = mailSenderMock.simpleMessage;
        assertThat(msg.getFrom(), is("[email protected]"));
        assertNotNull(msg.getTo());
        assertThat(msg.getTo().length, is(1));
        assertThat(msg.getTo()[0], is("[email protected]"));
        assertThat(msg.getSubject(), is("Hello World"));
        assertThat(msg.getText(), is("Welcome to my world."));
    }
}

In the sample, the mock class is written as an inner class, but you can create a class file individually. You can make it with Mockito.mock, though. It was the end of the story that Mockito was annoying, but ... (Mockito.mock () may be used for classes that do not have to do anything.)

The class under test in the sample is too simple, so it doesn't seem to make much difference, but I thought it would be easy not to mock it with Mockito. Also, since it no longer depends on SpringBoot, JUnit will not work due to property settings or lack of DB.

Such a thing

For example, you can have about 3 @Autowired and change the behavior of only one for unit testing. In this case, set it to @ SpringBootTest and @Autowired in the test class the class that does not change the behavior.

In this case, it depends on Spring Boot. I want to write test code like an integration test from Controller, but there is an obstructive class! At times, I think this method is also good.

MailServiceTest.java


@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class MailServiceTest {
    @Autowired
    HogeSerice hogeService;
    @Autowired
    FugaService fugaService;

    private class MailSenderMock {
        //Abbreviation
    }

    @Test
    public void test() {

        //Pass the mock class to create an instance
        MailSenderMock mailSenderMock = new MailSenderMock();
        MailService mailService = new MailService(mailSenderMock, hogeService, fugaService);
    //Abbreviation

Recommended Posts

Isn't unit testing easier with constructor injection? Talk
Testing with com.google.testing.compile
[Java] How to omit spring constructor injection with Lombok