[Spring] 단위테스트 @InjectMocks 사용방법

2024. 8. 17. 17:29·Spring/테스트 코드
반응형
 
 
 

단위테스트에서 @InjectMocks, @Mock, @Spy에 대한 것을 알아봤다.

📌 서론

요즘 개발하면서 단위 테스트를 많이 작성하는데 갑작스레 궁금증이 생겼다.

단위 테스트에서는 주로 @SpringBootTest가 아니라 @InjectMocks를 사용하는데 이 애노테이션의 이름만 보면 "Inject + Mocks = 목 객체들을 주입한다." 이렇게 해석되는데 이것도 객체에 Mock을 주입하는 거니까 스프링의 의존성 주입과는 뭐가 다를지 알아보고 싶었다.

 

이렇게 시작된 궁금증으로 직접 단위 테스트를 작성해 보면서 주로 보게 되는 @InjectMocks, @Mock, @Spy 이것들의 관계성을 알아봤다.

 

또한 테스트 과정에서 인터페이스는 @Mock을 사용해야 했는데 왜 @Spy는 사용할 수 없는지 이것에 대해서도 실험해 보고 그 내용을 작성했다.

 

내용에는 잘못된 부분이 있을 수도 있지만 이 글이 단위 테스트를 작성하는데 도움이 되었으면 좋겠다.

 

1. 테스트에 사용된 코드 설명

테스트를 위한 서비스 클래스

  • 헥사고날 아키텍처로 작성된 서비스 로직이다.
  • curd에 대한 useCase를 모두 구현했지만 이 글을 작성하기 위해 create에 대한 메서드만 남겨두고 모두 제거했다.
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class MemberService implements CreateMemberUseCase, FindMemberUseCase, UpdateMemberUseCase, DeleteMemberUseCase {

    private final CreateMemberPort createMemberPort;
    private final FindMemberPort findMemberPort;
    private final DeleteMemberPort deleteMemberPort;

    private final MemberMapper memberMapper;


    /**
     * @param member 회원
     * @return 생성된 회원
     * @apiNote 회원 생성
     */
    @Override
    public MemberResponseDTO createMember(Member member) {
        member.checkCreateRequiredValue();
        Member savedMember = createMemberPort.createMember(member);
        return memberMapper.domainToResponseDTO(savedMember);
    }
    
}

 

도메인 내부 비즈니스 로직 (데이터 검증 작업)

  • 멤버 도메인 내부에는 회원 가입 시 필수값을 체크하는 비즈니스 로직을 작성해 뒀다.
  • 비즈니스 설명: 도메인 모델 내부에서 필수 데이터를 검증하고, 규칙을 어기는 경우 예외를 발생시킨다.
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Member {

    private Long id;               // PK
    private String email;          // 이메일
    private String password;       // 소셜 로그인 사용자는 NULL일 수 있음
    private String name;           // 이름
    
    // ... 멤버 변수 생략

    /**
     * @apiNote 회원 가입시 필수 값 체크
     */
    public void checkCreateRequiredValue() {
        if (ObjectUtils.isEmpty(this.email)) {
            throw new MemberException(ErrorCode.MEMBER_CREATE_EMAIL_NOT_FOUND);
        }
        if (ObjectUtils.isEmpty(this.password)) {
            throw new MemberException(ErrorCode.MEMBER_CREATE_PASSWORD_NOT_FOUND);
        }
        if (ObjectUtils.isEmpty(this.name)) {
            throw new MemberException(ErrorCode.MEMBER_CREATE_NAME_NOT_FOUND);
        }
    }
    
}

 

테스트 코드는 다음과 같다.

  • 하단의 테스트는 회원 생성에 메서드에 대한 단위 테스트를 작성한 코드다.
  • happy (성공) 케이스와 bad (예외 상황)에 대한 케이스로 분리하여 작성하였다.
@ExtendWith(MockitoExtension.class)
@DisplayName("[Service] 멤버 서비스 단위 테스트")
class MemberServiceTest {

    @Mock private CreateMemberPort createMemberPort;
    @Mock private FindMemberPort findMemberPort;
    @Mock private DeleteMemberPort deleteMemberPort;
    @Mock private MemberMapper memberMapper;
    
    @InjectMocks    
    private MemberService sut;

    @DisplayName("[happy] 모든 데이터가 제공되면 회원이 성공적으로 생성된다.")
    @Test
    void createMember() {
        //given
        Member member = Member.builder().id(1L).name("test").email("Test@test.com").password("1234").build();
        MemberResponseDTO responseDTO = MemberResponseDTO.builder().id(1L).name("test").email("Test@test.com").password("1234").build();

        given(createMemberPort.createMember(any(Member.class))).willReturn(member);
        given(memberMapper.domainToResponseDTO(any(Member.class))).willReturn(responseDTO);

        // when
        MemberResponseDTO result = sut.createMember(member);

        // then
        Assertions.assertThat(result).isNotNull();
        Assertions.assertThat(result.getId()).isEqualTo(1L);
    }

    @DisplayName("[bad] 이메일 데이터가 누락되면 회원 생성에 실패한다.")
    @Test
    void createMemberException1() {
        //given
        Member member = Member.builder().id(1L).name("test").password("1234").build();

        // when & then
        Assertions.assertThatThrownBy(() -> sut.createMember(member))
                .isInstanceOf(MemberException.class)
                .hasMessageContaining(ErrorCode.MEMBER_CREATE_EMAIL_NOT_FOUND.getMessage());
    }

    @DisplayName("[bad] 이름 데이터가 누락되면 회원 생성에 실패한다.")
    @Test
    void createMemberException2() {
        //given
        Member member = Member.builder().id(1L).email("test@test.com").password("1234").build();

        // when & then
        Assertions.assertThatThrownBy(() -> sut.createMember(member))
                .isInstanceOf(MemberException.class)
                .hasMessageContaining(ErrorCode.MEMBER_CREATE_NAME_NOT_FOUND.getMessage());
    }

    @DisplayName("[bad] 비밀번호 데이터가 누락되면 회원 생성에 실패한다.")
    @Test
    void createMemberException3() {
        //given
        Member member = Member.builder().id(1L).name("test").email("test@test.com").build();

        // when & then
        Assertions.assertThatThrownBy(() -> sut.createMember(member))
                .isInstanceOf(MemberException.class)
                .hasMessageContaining(ErrorCode.MEMBER_CREATE_PASSWORD_NOT_FOUND.getMessage());
    }

}

 

2. 테스트 코드 분석

happy 케이스 테스트 코드를 분석해 보자

  • 회원가입에 필요한 모든 데이터가 전달되고 이 테스트는 문제없이 성공하게 된다.
  • port, mapper의 동작은 BDDMockito를 사용하여 given().willReturn()으로 동작을 정의해 줬다. 지금부터 자세히 알아보자.
@ExtendWith(MockitoExtension.class)
@DisplayName("[Service] 멤버 서비스 단위 테스트")
class MemberServiceTest {

    @Mock private CreateMemberPort createMemberPort;
    @Mock private FindMemberPort findMemberPort;
    @Mock private DeleteMemberPort deleteMemberPort;
    @Mock private MemberMapper memberMapper;
    
    @InjectMocks    
    private MemberService sut;

    @DisplayName("[happy] 모든 데이터가 제공되면 회원이 성공적으로 생성된다.")
    @Test
    void createMember() {
        //given
        Member member = Member.builder().id(1L).name("test").email("Test@test.com").password("1234").build();
        MemberResponseDTO responseDTO = MemberResponseDTO.builder().id(1L).name("test").email("Test@test.com").password("1234").build();

        given(createMemberPort.createMember(any(Member.class))).willReturn(member);
        given(memberMapper.domainToResponseDTO(any(Member.class))).willReturn(responseDTO);

        // when
        MemberResponseDTO result = sut.createMember(member);

        // then
        Assertions.assertThat(result).isNotNull();
        Assertions.assertThat(result.getId()).isEqualTo(1L);
    }

}

 

테스트 클래스 최상단을 확인해 보자

  • 코드를 보면 클래스 최상단에 @ExtendWith(MockitoExtension.class)를 적어준다.
  • @ExtendWith(MockitoExtension.class)는 JUnit 5에서 Mockito를 확장하여 사용할 수 있도록 해주는 애노테이션이다. 이 애노테이션을 통해, Mockito의 @Mock, @InjectMocks, @Spy와 같은 기능들이 JUnit 테스트에서 자동으로 동작하게 된다.
@ExtendWith(MockitoExtension.class)
@DisplayName("[Service] 멤버 서비스 단위 테스트")
class MemberServiceTest {
    
}

 

테스트 클래스 내부에 선언된 멤버 변수를 확인해 보자

  • 서비스 클래스의 필드에 선언된 클래스들은 테스트 클래스 내부에도 그대로 작성되어 있다.
  • 다른 점은 테스트 클래스 필드에 선언된 클래스들은 @Mock 애노테이션을 사용한다.
  • 제일 중요한 테스트 메서드를 가진 클래스는 @InjectMocks 애노테이션을 사용하고 있다.
  • 참고로 sut = system under test다.
@ExtendWith(MockitoExtension.class)
@DisplayName("[Service] 멤버 서비스 단위 테스트")
class MemberServiceTest {

    @Mock private CreateMemberPort createMemberPort;
    @Mock private FindMemberPort findMemberPort;
    @Mock private DeleteMemberPort deleteMemberPort;
    @Mock private MemberMapper memberMapper;
    
    @InjectMocks    
    private MemberService sut;
    
}

 

3. @InjectMocks, @Mock이 뭔데?

@InjectMocks

  • @InjectMocks은 Mockito가 자동으로 모킹 된 객체(@Mock으로 선언된 객체)를 주입해 주는 애노테이션이다. 이로 인해 테스트하려는 객체(예: 서비스 클래스)에서 필요로 하는 의존성 필드들을 자동으로 주입해 준다. 이 애노테이션을 사용하면, 테스트하려는 객체의 모든 의존성이 자동으로 주입되므로 수동으로 의존성을 설정할 필요가 없다.

@Mock

  • @Mock은 Mockito가 모킹 한 객체를 생성하기 위해 사용되는 애노테이션이다. 모킹 된 객체는 실제 객체의 동작을 시뮬레이션할 수 있으며, 이를 통해 테스트 시 특정 객체의 메서드를 원하는 방식으로 동작하도록 제어할 수 있다. 이로 인해 외부 의존성(데이터베이스, 네트워크, 파일 시스템 등)에 대한 접근 없이 테스트를 수행할 수 있다.
  • @Mock에 대한 상세한 설명은 하단의 4번 목차에서 확인할 수 있다.

 

테스트 코드를 다시 보자

  • 클래스 최상단에 @ExtendWith를 작성해서 @Mock, @InjectMocks, @Spy와 같은 기능들이 동작한다.
  • 하단의 @InjectMocks를 적어준 클래스는 실제 테스트를 하려는 MemberService 클래스이고 이 클래스는 @InjectMocks을 통해 @Mock, @Spy로 선언된 필드를 자동으로 주입받는다.
  • 나머지 @Mock을 사용하는 클래스들은 MemberService 클래스에서 필요로 하는 의존성 클래스들이다. 이 @Mock을 적어줘야만  @InjectMocks에서 이 클래스들을 의존성 주입 해준다.
@ExtendWith(MockitoExtension.class)
@DisplayName("[Service] 멤버 서비스 단위 테스트")
class MemberServiceTest {

    @Mock private CreateMemberPort createMemberPort;
    @Mock private FindMemberPort findMemberPort;
    @Mock private DeleteMemberPort deleteMemberPort;
    @Mock private MemberMapper memberMapper;
    
    @InjectMocks    
    private MemberService sut;
    
}

 

궁금증이 생겼다. 만약 테스트 하려는 실제 클래스가 필요로 하지 않는 의존성 객체들을 @Mock으로 선언한다면 어떻게 될까?

  • @Mock을 사용해서 MemberService에서는 주입받지 않는 클래스 2개를 선언해 봤다. 이후 테스트 코드를 실행해 봤더니 문제없이 성공한다.
  • 알아봤더니 @InjectMocks가 주입을 시도하는 객체(@Mock 객체)가 주입될 클래스(테스트할 메서드를 가진 클래스)의 필드에 없으면, 그 @Mock 객체는 그냥 무시된다. 따라서, 필요하지 않은 의존성을 @Mock으로 선언했다고 해서 테스트가 실패하지는 않는다.
  • 그래도 테스트 코드의 가독성을 위해서 정말 테스트에 필요한 의존성 객체만 @Mock으로 선언하도록 하자.
@ExtendWith(MockitoExtension.class)
@DisplayName("[Service] 멤버 서비스 단위 테스트")
class MemberServiceTest {

    @Mock private CreateMemberPort createMemberPort;
    @Mock private FindMemberPort findMemberPort;
    @Mock private DeleteMemberPort deleteMemberPort;
    @Mock private MemberMapper memberMapper;

    // 이 2가지 객체는 실제 서비스에서는 선언되어 있지 않다.
    @Mock private AuthService authService;
    @Mock private RefreshTokenMapper refreshTokenMapper;
    
    @InjectMocks private MemberService sut;

}

 

4. @InjectMocks에 @Spy를 쓰면 뭐가 다른데?

@Mock과 @Spy의 차이점

  • @InjectMocks는 Mockito에서 주입할 객체를 자동으로 생성하고, 주입할 클래스의 필드에 @Mock 또는 @Spy로 선언된 객체들을 주입한다. 그러나 @Mock과 @Spy의 차이는 객체의 동작 방식에 있다. 이를 이해하면 @InjectMocks와 함께 @Spy를 사용할 때 어떤 차이가 발생하는지 알 수 있다.

 

@Mock

  • 인터페이스에 적용 가능.
  • 객체의 모든 메서드를 모킹(기본적으로 아무 동작도 하지 않음).
  • 명시적으로 동작을 설정해야만 동작함.

@Spy

  • 구체적인 구현체에만 적용 가능.
  • 객체의 실제 메서드가 호출되며, 필요시 특정 메서드만 모킹 가능.
  • 인터페이스에는 적용할 수 없음.

 

@Spy를 테스트 코드에 사용해 보자

  • 기존 테스트 코드에서 @Mock으로 사용하던 memberMapper를 @Spy로 변경했다.
  • 여기서 자세히 보면 또 다른 점이 있는데 @Spy를 사용하게 되었기에 memberMapper 인터페이스를 MemberMapperImpl 구현체로 변경했다는 점이다. (인터페이스를 사용하면 오류가 발생하니 주의하자!)
  • 기존 테스트에서는 given().willReturn()으로 memberMapper 인터페이스의 메서드 동작을 정의했지만 이제는 실제 메서드가 알아서 처리해 줄 것이기에 given() 코드를 제거했다. (spy는 필요시 실제 동작 대신 stub을 해줘도 된다.)
@ExtendWith(MockitoExtension.class)
@DisplayName("[Service] 멤버 서비스 단위 테스트")
class MemberServiceTest {

    @Mock private CreateMemberPort createMemberPort;
    @Mock private FindMemberPort findMemberPort;
    @Mock private DeleteMemberPort deleteMemberPort;

    // spy로 작성
    @Spy private MemberMapperImpl memberMapper;

    @InjectMocks private MemberService sut;

    @DisplayName("[happy] 모든 데이터가 제공되면 회원이 성공적으로 생성된다.")
    @Test
    void createMember() {
        //given
        Member member = Member.builder().id(1L).name("test").email("Test@test.com").password("1234").build();

        given(createMemberPort.createMember(any(Member.class))).willReturn(member);

        // when
        MemberResponseDTO result = sut.createMember(member);

        // then
        Assertions.assertThat(result).isNotNull();
        Assertions.assertThat(result.getId()).isEqualTo(1L);
    }

}

 

5. 인터페이스에는 @Spy가 아닌 @Mock을 적용하는 이유

메서드 시그니처 기반

  • 인터페이스는 메서드 시그니처(메서드의 이름, 반환 타입, 파라미터 목록 등)를 정의한다. 따라서, 인터페이스에 @Mock을 적용하면, Mockito는 이 인터페이스의 모든 메서드에 대해 기본 동작을 설정할 수 있다.

 

프록시 객체 생성

  • Mockito는 인터페이스에 대한 프록시 객체를 생성한다. 이 프록시 객체는 인터페이스에 정의된 메서드를 호출할 수 있지만, 실제로는 구현이 없기 때문에, 기본적으로 아무 동작도 하지 않고, 반환 타입에 따른 기본값을 반환한다.

구현체와의 차이

  • 구현체에 @Mock을 사용하면, 해당 클래스의 실제 메서드 구현이 무시되고, 모킹 된 결과를 반환하게 된다. 하지만 인터페이스는 애초에 구현체가 없기 때문에, 메서드 시그니처에 따라 기본값을 반환한다.

결과 값의 기본값

  • 만약 인터페이스에 @Mock을 적용하면, 다음과 같은 기본값을 반환한다.
    • boolean: false
    • int: 0
    • Object: null
    • Collection: 빈 컬렉션 등

@Spy는 실제 동작을 기반으로 한다

  • @Spy는 특정 메서드를 모킹 하기 전에는 기본적으로 실제 객체의 메서드가 호출되도록 한다. 인터페이스는 메서드의 구현이 없으므로, @Spy가 수행할 실제 동작이 존재하지 않게 된다. 그래서 @Spy는 인터페이스가 아닌, 실제 구현체가 있는 클래스에만 사용할 수 있다.

최종 정리

  • 인터페이스에 @Mock을 적용하면, 그 인터페이스의 모든 메서드를 호출할 수 있지만, 기본적으로는 아무런 동작도 수행하지 않으며, 반환 타입에 맞는 기본값을 반환한다. 이는 인터페이스의 메서드 시그니처가 존재하기 때문에 가능하며, 이 메서드 시그니처를 기반으로 Mockito가 프록시 객체를 생성하기 때문이다.

  • 반면 @Spy는 실제 객체를 사용하고, 그 객체의 메서드를 호출할 때 실제로 동작하도록 하면서, 특정 메서드만 모킹 할 수 있도록 해준다. 하지만 인터페이스는 단순히 메서드의 시그니처만 정의할 뿐, 실제 구현체가 없기 때문에, @Spy로 감싸려면 구체적인 클래스(즉, 실제 객체)가 필요하다. 따라서 @Spy는 구체적인 동작을 포함하는 실제 객체가 있어야 하기 때문에 인터페이스에는 사용할 수 없고, 대신 클래스에 적용하여 부분 모킹을 사용할 수 있다.

 

6. @InjectMocks는 실제 테스트할 객체가 필요로 하는 모든 의존성을 주입받아야 하나?

테스트하고자 하는 실제 서비스 코드를 다시 확인해 보자

  • 클래스를 살펴보면 4개의 외부 클래스를 의존성 주입으로 받고 있는 것을 알 수 있다.
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class MemberService implements CreateMemberUseCase, FindMemberUseCase, UpdateMemberUseCase, DeleteMemberUseCase {

    private final CreateMemberPort createMemberPort;
    private final FindMemberPort findMemberPort;
    private final DeleteMemberPort deleteMemberPort;
    private final MemberMapper memberMapper;
    
}

 

그럼 테스트에서 2개만 주입해 주면 어떻게 될까?

  • 나는 스프링의 의존성 주입과 비슷하게 동작하지 않을까? 이런 생각을 했었다.
  • 그래서 4개가 다 주입되어야 동작할 것이라고 생각하고 2개의 의존성을 주석 처리하고 테스트를 진행해 봤다.
  • 결과는 테스트 메서드에서 필요로 하는 2개의 의존성만 @Mock으로 주입해 줘도 테스트에 성공한다.
@ExtendWith(MockitoExtension.class)
@DisplayName("[Service] 멤버 서비스 단위 테스트")
class MemberServiceTest {

    @Mock private CreateMemberPort createMemberPort;
    @Mock private MemberMapper memberMapper;
    
    
//    @Mock private FindMemberPort findMemberPort;
//    @Mock private DeleteMemberPort deleteMemberPort;


    @InjectMocks private MemberService sut;

    @DisplayName("[happy] 모든 데이터가 제공되면 회원이 성공적으로 생성된다.")
    @Test
    void createMember() {
        //given
        Member member = Member.builder().id(1L).name("test").email("Test@test.com").password("1234").build();
        MemberResponseDTO responseDTO = MemberResponseDTO.builder().id(1L).name("test").email("Test@test.com").password("1234").build();

        given(createMemberPort.createMember(any(Member.class))).willReturn(member);
        given(memberMapper.domainToResponseDTO(any(Member.class))).willReturn(responseDTO);

        // when
        MemberResponseDTO result = sut.createMember(member);

        // then
        Assertions.assertThat(result).isNotNull();
        Assertions.assertThat(result.getId()).isEqualTo(1L);
    }
    
}

 

7. 스프링과 Mockito의 의존성 주입의 차이점

스프링의 의존성 주입 (DI)

  • 스프링 DI는 애플리케이션의 전반적인 의존성을 관리하고 주입하는 방법이다. 이를 통해 애플리케이션이 실행될 때 필요한 모든 의존성이 주입된다.

  • 스프링의 생성자 주입 방식에서는 모든 final 필드가 반드시 초기화되어야 하며, 스프링은 애플리케이션 실행 시 의존성을 자동으로 주입한다. 만약 어떤 의존성이 주입되지 않으면, 스프링은 오류를 발생시키며 애플리케이션이 정상적으로 실행되지 않는다.

  • 스프링에서 null이 주입되는 경우, 이는 의존성 미주입과 동일하게 간주되며, 런타임에서 오류가 발생할 수 있다.

Mockito의 의존성 주입

  • Mockito는 주로 단위 테스트에서 객체 간의 의존성을 모킹(mocking)하고 주입하는 데 사용된다. 이로 인해 실제 의존성을 모킹 하여 독립적으로 테스트할 수 있다.

  • 주입할 클래스의 필드가 final로 선언된 경우 @InjectMocks는 클래스의 생성자를 통해 필요한 의존성들을 주입한다. 만약 생성자에서 요구하는 필드가 @Mock으로 선언되지 않았거나 주입할 객체가 없다면, 그 필드는 null로 전달된다. 즉, 생성자를 호출할 때, 주입 가능한 @Mock 객체는 주입되고, 나머지는 null로 주입되어 객체가 생성된다.

결론

  • 스프링 DI는 애플리케이션 실행 시 모든 의존성이 주입되어야 하며, 주입되지 않은 의존성에 대해 오류를 발생시킨다.
  • Mockito는 @InjectMocks를 사용하면 사용자가 모킹(@Mock)한 의존성 필드만 주입하고, 모킹(@Mock) 하지 않은 의존성 필드는 자동으로 null로 주입해서 sut 객체를 생성한다.
반응형

'Spring > 테스트 코드' 카테고리의 다른 글

[Spring] 예외 테스트의 중요성: 바인딩 오류  (5) 2024.02.03
Spring 통합테스트: Validation 문제 해결과 깊은 고민  (30) 2024.01.10
[Spring] 테스트: @ParameterizedTest 사용방법  (29) 2023.12.24
[Spring] 테스트 코드: @MockBean/@SpyBean 사용방법  (66) 2023.12.24
[Spring] 테스트 코드: @SpyBean으로 @EventListener 검증하기  (40) 2023.12.23
'Spring/테스트 코드' 카테고리의 다른 글
  • [Spring] 예외 테스트의 중요성: 바인딩 오류
  • Spring 통합테스트: Validation 문제 해결과 깊은 고민
  • [Spring] 테스트: @ParameterizedTest 사용방법
  • [Spring] 테스트 코드: @MockBean/@SpyBean 사용방법
Stark97
Stark97
소통 및 문의: dig04059@gmail.com (편하게 연락주세요!) 링크드인 소통이나 커피챗도 환영합니다!
  • Stark97
    오늘도 개발중입니다
    Stark97
  • 전체
    오늘
    어제
    • 분류 전체보기 (250)
      • 개발지식 (20)
        • 스레드(Thread) (8)
        • WEB, DB, GIT (3)
        • 디자인패턴 (8)
      • JAVA (21)
      • Spring (88)
        • Spring 기초 지식 (35)
        • Spring 설정 (6)
        • JPA (7)
        • Spring Security (17)
        • Spring에서 Java 활용하기 (8)
        • 테스트 코드 (15)
      • 아키텍처 (6)
      • MSA (15)
      • DDD (12)
      • gRPC (9)
      • Apache Kafka (19)
      • DevOps (23)
        • nGrinder (4)
        • Docker (1)
        • k8s (1)
        • 테라폼(Terraform) (12)
      • AWS (32)
        • ECS, ECR (14)
        • EC2 (2)
        • CodePipeline, CICD (8)
        • SNS, SQS (5)
        • RDS (2)
      • notion&obsidian (3)
      • 채팅 서비스 (1)
      • AI 탐험대 (1)
      • 팀 Pulse (0)
  • 링크

    • notion기록
    • 깃허브
    • 링크드인
  • hELLO· Designed By정상우.v4.10.0
Stark97
[Spring] 단위테스트 @InjectMocks 사용방법
상단으로

티스토리툴바