@Test
public void Services_should_only_be_accessed_by_Controllers() {
// 1. 특정 패키지에 해당하는 클래스를 (바이트코드를 통해) 읽어들인다
JavaClasses importedClasses = new ClassFileImporter().importPackages("com.mycompany.myapp");
// 2. 확인할 규칙을 정의한다
ArchRule myRule = classes()
.that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
// 3. 읽어들인 클래스들이 그 규칙을 잘 따르는지 확인한다
myRule.check(importedClasses);
}
ArchUnit의 한가지 아쉬운점은 DisplayName을 설정하지 못한다
junit engine을 확장하여 archunit-junit 모듈을 만든 것이기 때문이다 (archunit engine을 따로 사용)
GenericContainer(dockerImageName): 이미지 이름만 있으면 컨테이너를 만드는 것이 가능하다. 이 경우 먼저 로컬에서 이미지를 찾아보고, 로컬에 없으면 퍼블릭한 원격에서 찾아서 가져온다.
withEnv: 환경 변수 세팅이 필요하다.
withExposedPorts: 포트 노출 -> 일반적으로는 컨테이너가 랜덤하게 매핑해준다
waitingFor: 해당 컨테이너가 띄워졌는지 확인 후 테스트 실행
TestContainers의 로그 살펴보기
getLogs(): 모든 로그를 보는 방법
followOutput(new Slf4jLogConsumer(log)): 로그 스트리밍
컨테이너 내부의 정보를 테스트 코드에서 활용하는 방법
ApplicationContextInitializer 스프링 ApplicationContext를 프로그래밍으로 초기화 할 때 사용할 수 있는 콜백 인터페이스로, 특정 프로파일을 활성화 하거나, 프로퍼티 소스를 추가하는 등의 작업을 할 수 있다.
TestPropertyValues 테스트용 프로퍼티 소스를 정의할 때 사용한다.
Environment 스프링 핵심 API로, 프로퍼티와 프로파일을 담당한다.
Testcontainer를 사용해서 생성한 컨테이너에 대해 ApplicationContextInitializer를 구현하여 생성된 컨테이너에서 정보를 Environment에 넣어준다.
그 다음 @ContextConfiguration을 사용해서 아까 구현했던 ApplicationContextInitializer 구현체를 등록해주면, 테스트 코드에서 Environment, @Value, @ConfigurationProperties 등 다양한 방법으로 프로퍼티를 사용할 수 있게 된다.
단위 테스트를 생각할 때, 클래스나 오브젝트, 또는 행동과 연관된 로직이라고 다양하게 생각할 수 있다.
나중에 테스트를 작성하게 되면 단위테스의 단위를 어떻게 정의할 것인가, Mock은 어느 범위에서 적용할 것인가 등을 함께 정하는 것이 좋다고 함
Mockito의 경우 스프링 부트를 사용하게 되면 spring-boot-starter-test의 의존성을 따라 자동으로 들어오게 된다.
만약 이 것을 사용하지 않은 경우엔 아래 2가지 의존성 추가가 필요하다.
mockito-junit-jupiter: junit test에서 mockito를 연동해서 사용할 수 있는 추가적인 확장 구현체를 제공하는 라이브러리
mockito-core: mockito가 제공하는 기본적인 기능들이 들어있는 라이브러리
Mock은 그럼 언제 사용하냐면?
외부 api등을 호출할 때, 어떻게 답이 오는지 가정 등을 하고 mock 객체로 만들어 이 것이 어떻게 동작하는지 테스트할 경우 등 사용한다.
또, 내가 만들고 있는 코드가 의존하는 클래스의 구현체는 없지만 인터페이스는 있고, 그 인터페이스 기반으로 코드를 작성할 때 그 것을 확인하기 위해 사용되기도 한다.
아래와 같은 두 가지 방법으로 Mock 객체를 만들 수 있다.
Mockito.mock: 생성자로 주입하는 방법
@Mock + @ExtendWith(MockitoExtension.class) - 함수 밖에 테스트 클래스의 필드로 주입할 수도 있고, 파라미터 전달도 가능하다
Mock 객체를 만든 후에는 Mock 객체의 행동을 조작해야한다. 이를 Stubbing이라고 한다.
모든 Mock 객체의 행동은 기본적으로 아래와 같이 행동한다.
리턴 타입이 있다면 Null 리턴한다. (Optional 타입은 Optional.empty 리턴)
Primitive 타입은 프리미티브 기본 값을 따름
콜렉션은 비어있는 콜렉션으로 만들어준다
Void 메소드는 예외를 던지지 않고 아무런 일도 발생하지 않는다
Mock 객체를 조작하기 위해서는
Mockito.when(/*조건*/).thenReturn(/*객체*/): 조건에 해당한다면 무조건 어떠한 객체를 리턴하라
Mockito.when(/*조건*/).thenReturn(/*객체*/).thenThrow~~.thenReturn~~: 메소드가 여러 번 호출될 때 stubbing을 같은 매개변수로 호출 되더라도 호출되는 순서에 따라 다르게 mocking. 즉, 동일한 조건에 대해 다양한 객체/예외를 리턴하도록 설정해 둘 수 있다.
doThrow(/*Exception*/).when(/*조건*/): 조건에 해당한다면 어떠한 예외를 던져라
마지막으로 Mockito로 만든 Mock 객체가 어떻게 사용됐는지(객체에 어떤 일이 일어났는지) 확인하는 방법이다.
verify(memberService, times(1)).notify(study): 예를 들어 mock객체 memberService의 함수 notify가 1번만 호출되어야 한다
Mockito에서는 별개로 BDD 스타일의 테스트를 할 수 있는 방법을 BddMockito 클래스를 통해 제공한다.
BDD (Behaviour-Driven Development): 애플리케이션이 어떻게 “행동”해야 하는지에 대한 공통된 이해를 구성하는 방법으로, TDD에서 창안했다.
BDD는 시나리오를 기반으로 테스트 케이스를 작성하며 함수 단위 테스트를 권장하지 않는다. 이 시나리오는 개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 레벨을 권장한다. 하나의 시나리오는 Given, When, Then 구조를 가지는 것을 기본 패턴으로 권장하며 각 절의 의미는 다음과 같다.
Feature : 테스트에 대상의 기능/책임을 명시한다 Scenario : 테스트 목적에 대한 상황을 설명한다 Given : 시나리오 진행에 필요한 값을 설정한다 - 어떤 상황이 주어졌을 때 When : 시나리오를 진행하는데 필요한 조건을 명시한다 - 무엇인가를 하면 Then : 시나리오를 완료했을 때 보장해야 하는 결과를 명시한다 - ~~할 것이다
위의 내용을 개발 측면에서 더 간략하게 정리하면 테스트 대상의 상태 변화를 테스트하는 것이다. 테스트 대상은 A 상태에서 출발하며(Given) 어떤 상태 변화를 가했을 때(When) 기대하는 상태로 완료되어야 한다. (Then) 또는 Side Effect가 전혀 없는 테스트 대상이라면 테스트 대상의 환경을 A 상태에 두고(Given) 어떤 행동을 요구했을 때(When) 기대하는 결과를 돌려받아야 한다. (Then)
일반적으로 테스트를 실행하면 테스트에 이름을 표기하는 방법은 아래와 같다. (기본 표기 전략은 메소드 이름)
메소드 이름은 길어지면 snake case로 작성한다.
@DisplayNameGeneration: 메소드와 클래스 레퍼런스를 사용해서 테스트 이름을 표기하는 방법 설정하는 방법으로, 기본 구현체로 ReplaceUnderscores 제공 (ex.@DisplayNameGeneration(DisplayGenerator.ReplaceUnderscores.class))
@DisplayName: 어떤 테스트인지 테스트 이름을 보다 쉽게 표현할 수 있는 방법을 제공하는 애노테이션이다. @DisplayNameGeneration 보다 우선 순위가 높다.
한 테스트에서 여러 Assertion문이 있는 경우에는 앞에 있는 assert문이 실패할 경우 그 다음을 보지 않는데,
이 것을 한번에 실행해 주는 방법이 assertAll이다. 각 assert문을 람다식으로 묶어 주면 한번에 실행하여 결과를 알 수 있다.
assertAll(
() -> assertNotNull(study),
() -> assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면" + StudyStatus.DRAFT + "상태이다."),
() -> assertTrue(study.getLimit()>0, "스터디 최대 참석 가능 인원은 0명 이상이다.");
);
참고로, assert문이 실패했을 때 메세지를 supplier를 사용하여 나타내게 되면 테스트가 실패할 때만 연산이 일어나기 때문에 성능상 조금 더 이점이 있다.
이와 유사하게 테스트 태깅 등으로 그룹화를 할 수 있다.
@Tag: 테스트를 그룹화 ex) 테스트가 오래 걸려서 로컬에서 테스트하기 적절하지 않는지/테스트가 짧게 끝나는지 등
실행 방법
intellij에서 edit contiguration > test kind를 Tags로 바꾸기 > tag expression 설정해 주기
maven 설정: maven-surefire-plugin의 그룹을 설정해주기
조건에 따라 테스트를 실행할 수도 있다. (특정한 OS, 특정한 java version, 환경 변수 등)
assumeTrue(조건): 예를 들어 테스트의 환경변수가 로컬일 경우에만 실행할 때, 아래와 같이 진행할 수 있다.