본문 바로가기

Development/Java

[Spring Boot] Spring Boot Test에서 @Autowried와 @InjectMocks 사용

728x90

요즘 Unit Test에 대한 관심이 많아 졌으나 개인적으로 실제 대충 사용하여 잘 모르는 부분입니다.

특히 모듈로 분리하였을 경우 Test가 힘든 점이 있어 Test의 관련된 내용을 조금 정리하고자 합니다.

그 중 Spring Boot의 @Autowired와 Mockito의 @InjectMocks을 Spring Boot Tests에 의존성을 주입하여 사용하는 방법을 간단하게 테스트 진행할 것입니다.

테스트 주석의 이해

Mockito의 가장 일반적으로 사용되는 @Mock주석은테스트를 위한 종속성의 모의 인스턴스를 생성합니다. 이는 종종 @Mock으로 표시된 모의를 테스트 중인 대상 객체에 주입하는 @InjectMocks와 함께 사용되곤 합니다.

Spring Boot의 주석 @MockBean은 모의된 Spring 빈을 만드는데 도움이 도리수 있습니다. 모의된 빈은 컨텍스트의 다른 빈에서 사용할 수 있습니다. 게다가 Spring 컨텍스트가 Mocking없이 사용할 수 있는 빈을 스스로 만드는 경우 @Autowired 주석을 사용하여 주입할 수 있습니다.

예제 설정

1. 의존성(Dependencies)

Maven 의존성 설정

<!-- spring boot -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.2.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>3.2.5</version>
    <scope>test</scope>
</dependency>
<!-- mockito -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.11.0</version>
</dependency>

 

Gradle 의존성 설정

dependencies {
    // spring-boot
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // mokito
    implementation 'org.mockito:mockito-core:5.11.0'
}

2. DTO

서비스에서 사용할 DTO를 생성합니다.

public class Book {

    private String id;
    private String name;
    private String author;

    public Book(String id, String name, String author) {
        this.id = id;
        this.name = name;
        this.author = author;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

}

3. Service

DB(DataBase)을 담당하는 서비스를 정의 합니다.

단순한 테스트 이므로 실제 DB와 상관이 없이 사용한느 것처럼 유사하게 구성합니다.

 

@Service
public class DatabaseService {
    public Book findById(String id) {
        // querying a Database and getting a book
        return new Book("id","Name", "Author");
    }
}
 

위 Service(DatabaseServcie)를 의존하는 서비스를 생성합니다.

@Service
public class BookService {
    private final DatabaseService databaseService;
    private final ObjectMapper objectMapper;

    public BookService(DatabaseService databaseService, ObjectMapper objectMapper) {
        this.databaseService = databaseService;
        this.objectMapper = objectMapper;
    }

    String getBook(String id) throws JsonProcessingException {
        Book book = databaseService.findById(id);
        return objectMapper.writeValueAsString(book);
    }

}

 

getBook()이란 매서드는 DatabaseServcie를 사용하여 데이터베이스에서 Book을 가져와서 Jackson의 ObjectMapper를 활용하여 JSON 문자열로 변환하여 반환합니다.

즉, BookService는 DatabaseService와 ObjectMapper 2가지의 의존성을 가지고 있습니다.

테스트

1. @Mock 및 @InjectMocks 사용

@Mock을 사용하여 DatabaseService와 ObjectMapper 두 의존성을 모의(가상)하고, @InjectMocks를 사용하여 BookService에 주입합니다.

@ExtendWith(MockitoExtension.class)
class BookServiceMockAndInjectMocksUnitTest {

    @Mock
    private DatabaseService databaseService;

    @Mock
    private ObjectMapper objectMapper;

    @InjectMocks
    private BookService bookService;

    @Test
    void givenBookService_whenGettingBook_thenBookIsCorrect() throws JsonProcessingException {
        Book book1 = new Book("1234", "Inferno", "Dan Brown");
        when(databaseService.findById(eq("1234"))).thenReturn(book1);

        when(objectMapper.writeValueAsString(any())).thenReturn(new ObjectMapper().writeValueAsString(book1));

        String bookString1 = bookService.getBook("1234");
        Assertions.assertTrue(bookString1.contains("Dan Brown"));
        System.out.println("finish.");
    }
}

 

@ExtendWith(MockitoExtendsion.class) 애너테이션으로 테스트에서 객체를 모의하고 주입할 수 있습니다.

@Mock 애너테이션을 활용하여 DatabaseService 및 ObjectMapper를 모의 객체로 생성합니다.

@InjectMocks 애터네이션을 BookService에 사용하면 @Mocks로 선언되 모든 종속성이 BookService에 주입이 될 것입니다.

마지막으로 BookService의 getBook()를 테스트 합니다

주의할 것은 테스트할 서비스의 모든 종속성을 모의(Mock)하는 것이 필수입니다. 만약 하나라도 모의(Mock)하지 않았다면 NullPointerException이 발생할 것입니다.

2. @Autowired를 @MockBean과 함께 사용

@Mock 및 @InjectMocks을 사용한 예제(1번 예제)의 경우 두 종속성을 모의하였습니다. 하지만 일부 종속성을 모의하고 다른 종속성은 모의하지 않아야 할 수도 있습니다. ObjectMapper의 동작을 모의할 필요가 없고 DatabaseService만 모의 한다고 가정할 경우 테스트에서 Srping Context를 로드하고 있으므로 @Autowired와 @MockBean 주석을 조합하여 사용할 수 있습니다.

@SpringBootTest
public class BookServiceAutowiredAndMockBeanUnitTest {
    @MockBean
    private DatabaseService databaseService;

    @Autowired
    private BookService bookService;

    @Test
    void givenBookService_whenGettingBook_thenBookIsCorrect() throws JsonProcessingException {
        Book book1 = new Book("1234", "Inferno", "Dan Brown");
        when(databaseService.findById(eq("1234"))).thenReturn(book1);

        String bookString1 = bookService.getBook("1234");
        Assertions.assertTrue(bookString1.contains("Dan Brown"));
        System.out.println("finish.");
    }
}

 

@SpringBootTest 애너테이션으로 Srping Context에서 빈을 사용하게 됩니다.

DatabaseServcie에 @MockBean 애너테이션을 사용하고 BookService에 @Autowired를 사용하여 인스턴스를 가져옵니다.

BookService 빈이 주입 되면 실제 DatabaseService 빈은 모의된 빈으로 대체됩니다. 

ObjectMapper는 기존의 생성한 것과 동일하게 유지됩니다.

이제는 ObjectMapper에 대한 어떤 동작도 모의할 필요가 없습니다. 이 방법은 중첩된 빈의 동작을 테스트해야 하지만 모든 종속성을 모의하고 싶지 않을 때 유용합니다.

3. @Autowired와 @InjectMocks를 함게 사용

@MockBean 대신 @InjectMocks를 사용할 수도 있습니다.

@SpringBootTest
public class BookServiceAutowiredAndInjectMocksUnitTest {
    @Mock
    private DatabaseService databaseService;

    @Autowired
    @InjectMocks
    private BookService bookService;

    @Test
    void givenBookService_whenGettingBook_thenBookIsCorrect() throws JsonProcessingException {
        Book book1 = new Book("1234", "Inferno", "Dan Brown");

        MockitoAnnotations.openMocks(this);

        when(databaseService.findById(eq("1234"))).thenReturn(book1);
        String bookString1 = bookService.getBook("1234");
        Assertions.assertTrue(bookString1.contains("Dan Brown"));
    }
}

 

@MockBean 대신 @Mock을 사용하여 DatabaseService를 모의합니다. @Autowired외에도 BookService인스턴스에 @InjectMock 애너테이션을 추가합니다.

두 가지 애너테이션을 같이 사용하면 @InjectMocks은 모의된 종속성을 자동으로 주입하지 않고 자동 연결된 BookService 객체는 테스트가 시작될 때 주입됩니다.

하지만 나중에 테스트에서 MockitoAnnotations.openMocks() 매서드를 호출하여 DatabaseService의 모의 인스턴스를 주입할 수 있습니다. 이 매서든느 @InjectMocks로 표시된 필드를 찾아 모의 객체를 주입합니다.

DatabaseeService의 동작을 모의하기 직전에 테스트에서 호출합니다. 이 메서드는 모의를 사용할 때와 종ㅅ혹성에 실제 빈을 사용할 대를 동적으로 결장하고자 할 때 유용합니다.

 

접근 방식 비교

접근방법 설명 사용법
@Mock과 
@InjectMocks
Mockito의 @Mock 주석을 사용하여 종속성의 모의 인스턴스를 생성하고 @InjectMocks를 사용하여 이러한 모의 인스턴스를 테스트 중인 대상 객체에 주입합니다. 테스트 중인 클래스의 모든 종속성을 모의하려는 단위 테스트에 적합합니다.
@Autowired를 사용한
 @MockBean
Spring Boot의 @MockBean 어노테이션을 활용하여 모의 Spring 빈을 생성하고 @Autowired를 사용 하여 이러한 빈을 주입합니다. Spring Boot 애플리케이션에서 통합 테스트에 이상적입니다. Spring의 종속성 주입에서 다른 빈을 가져오는 동안 일부 Spring 빈을 모의할 수 있습니다.
@InjectMocks와 
@Autowired
Mockito의 @Mock 어노테이션을 사용하여 모의 인스턴스를 생성하고, @InjectMocks를 사용 하여 이러한 모의 인스턴스를 Spring을 사용하여 이미 자동으로 연결된 대상 빈에 주입합니다. Mockito를 사용하여 주입된 Spring Bean을 재정의하기 위해 일부 종속성을 일시적으로 모의해야 하는 시나리오에서 유연성을 제공합니다. Spring 애플리케이션에서 복잡한 시나리오를 테스트하는 데 유용합니다.

 

 

 

 

Mock을 잘 사용하면 멀티 모듈에서도 단위 테스트에 있어서 보다 효율적인 테스트가 가능할 것으로 보입니다.

적재적소에 사용한다면 분명히 효율적인 테스트를 함으로써 Test로 인한 생산성 저하를 막을 수 있지 않을까 싶습니다.

 

 

 

 

 

 

 

!! 만약 잘못된 부분이나 수정되어야 할 부분이 있으면 댓글로 말씀해 주세요 !!

 

 

 

 

 

 

 

 

 

 

 

 

참고: https://www.baeldung.com/spring-test-autowired-injectmocks

728x90
반응형