-1

I use Spring Boot 5 and JUnit in my project. I create a unit test to test the service.

Here is the service that I am testing:

@Service
@RequiredArgsConstructor
@Slf4j
public class BuilderServiceImpl implements BuilderService{

    @Autowired
    public AutoMapper autoMapper;

    private final BuilderRepository builderRepository;
    private final AdminUserRepository adminUserRepository;


    @Override
    public BuilderDto getByEmail(String email){
    }

    @Override
    public List<BuilderMinDto> getAll() {}

    @Override
    public List<BuilderMinDto> getAll(int page, int size) {}

    @Override
    public SaveBuilderResponse create(Builder builder){

        var str = autoMapper.getDummyText();

        Builder savedBuilder = builderRepository.save(builder);

        return new SaveBuilderResponse(savedBuilder);
    }
}

And here is the test class that tests the service above:

@SpringBootTest
@RequiredArgsConstructor
@Slf4j
class BuilderServiceImplTest {

    @Mock
    private BuilderRepository builderRepository; 
    @Mock
    private AdminUserRepository adminUserRepository; 

    private  AutoCloseable autoCloseable;
    private  BuilderService underTest;

    @BeforeEach
    void setUp(){
        autoCloseable = MockitoAnnotations.openMocks(this);
        underTest = new BuilderServiceImpl(builderRepository,adminUserRepository);
    }

    @AfterEach
    void tearDown () throws Exception{
        autoCloseable.close();
    }

    @Test
    void getByEmail(){}

    @Test
    @Disabled
    void getAll() { }


    @Test
    @Disabled
    void testGetAll() {}

    @Test
    void create() {
        //given
        Builder builder = new Builder();
        builder.setName("John Johnson");
        builder.setCompanyName("Builders Test");
        builder.setEmail("test@builders.com");

        //when
        underTest.create(builder);

        //then
        ArgumentCaptor<Builder> builderArgumentCaptor = ArgumentCaptor.forClass(Builder.class);

        verify(builderRepository)
                .save(builderArgumentCaptor.capture());

        Builder captureBuilder = builderArgumentCaptor.getValue();

        assertThat(captureBuilder).isEqualTo(builder);
    }
}

When I start to run the test class the create method in BuilderServiceImpl fired and on this row:

        var str = autoMapper.getDummyText();
        

I get NullPointerException(autoMapper instance is null).

Here is the definition of AutoMapper class:

@Component
@Slf4j
@RequiredArgsConstructor
public class AutoMapper {

    public String getDummyText(){
        return  "Hello From AutoMapper.";
    }
}

As you can see I use @Component annotation to register the AutoMapper class to the IoC container and Autowired annotation to inject it into autoMapper property in BuilderServiceImpl class.

Why autoMapper instance is null? How can I make autoMapper to be initialized?

halfer
  • 19,824
  • 17
  • 99
  • 186
Michael
  • 13,950
  • 57
  • 145
  • 288

3 Answers3

0

Add @Autowire annotation Annotations on below fields. Error due your not initialized below object In BuilderServiceImpl

@Autowire
private final BuilderRepository builderRepository;

@Autowire
private final AdminUserRepository adminUserRepository;
S. Anushan
  • 728
  • 1
  • 6
  • 12
  • Can you give the explanation, how you call builderRepository.save() method without object initialization in service class? If annotate with Autowire then Spring context will inject. And give comment for the down vote? – S. Anushan Jul 18 '21 at 09:38
0

In order to make @Autowire work you have to use the instance of BuilderServiceImpl (object under test) created by spring itself.

When you create the object like this (by yourself, manually):

 @BeforeEach
    void setUp(){
      ....
        underTest = new BuilderServiceImpl(builderRepository,adminUserRepository);
    }

Spring doesn't know anything about this object, hence Autowiring won't work

Another thing that might be useful:

You've used @Mock for BuilderRepository and AdminUserRepository. This are plain mockito annotation, and if you're using an integration/system test that runs the spring under the hood, probably this is not what you want:

Surely, it will create a mock, but it won't put it onto an application context, and won't substitute the beans of these classes that might have been created by spring.

So if this is what you want to achieve, you should use @MockBean instead. This annotation belongs to Spring Testing framework rather than a plain mockito annotation.

All-in-all you might end up with something like this:

@SpringBootTest 
class MyTest
{
   @MockBean
   BuilderRepository builderRepo;
   @MockBean
   AdminUserRepository adminUserRepo;

   @Autowired  // spring will inject your mock repository implementations
               // automatically 
   BuilderServiceImpl underTest;

   @Test
   void mytest() {
      ...
   }
}
Mark Bramnik
  • 39,963
  • 4
  • 57
  • 97
  • Mark Thanks for your great post. Actually, I try to make a unit test. This is a reason why I use Mocks. – Michael Jul 18 '21 at 18:16
  • Yeah, I know, but its actually not a unit test. You basically run an integration test (the one that starts the spring infrastructure). In this case, you actually want to substitute the real beans with mock implementation and place them onto the application context, so that they'll be injected wherever needed. @MockBean does exactly that. The actual mock is still created with the help of Mockito under the hood, but the "magic" that spring adds is that it actually places this mock into the application context and really uses it for injection into other classes (like the one under test) – Mark Bramnik Jul 18 '21 at 18:23
  • Not for the sake of self-promotion or something :) but I answered a couple of years ago a question that describes the differences between the types of tests, those with spring, and those without in the answer in this post - I think you can read it and actually understand what are the differences between the different ways of test invocation: https://stackoverflow.com/questions/56712707/springboottest-vs-contextconfiguration-vs-import-in-spring-boot-unit-test/56712901#56712901 – Mark Bramnik Jul 18 '21 at 18:27
  • Mark as I understand because BuilderServiceImpl class relies on automapped class, it should be tested as an integration test? Because as I understand In unit testing each module is tested separately. – Michael Jul 18 '21 at 18:39
  • 1
    No, the unit, integration, system tests are just tools, you pick which type of testing do you want to apply in each case. Unit test is when you only test the `BuilderServiceImpl` without spring at all (unit tests must be fast, and loading spring takes time). Using Field Injection (Autowired on field) doesn’t allow proper unit testing, but you could pass the automapper as a parameter in constructor instead, and thus `BuilderServiceImpl` could be eligible for the unit testing. Integration testing is also required for many tasks, but again, its just a tool… – Mark Bramnik Jul 18 '21 at 18:45
  • Great explanation! Thank you! – Michael Jul 18 '21 at 18:49
-1

Why are you creating BuildService manually? If you do this, set AutoMapper manualy too.

@BeforeEach
    void setUp(){
        autoCloseable = MockitoAnnotations.openMocks(this);
        underTest = new BuilderServiceImpl(builderRepository,adminUserRepository);
        underTest.setAutoMapper(new AutoMapper());
    }

You are not using di.

Shakirov Ramil
  • 1,381
  • 9
  • 13
  • Thanks for the post. What is the setAutoMapper method? I don't see it on the API of BuilderServiceImpl. – Michael Jul 18 '21 at 17:16