ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [클린 아키텍처 07] 아키텍처 요소 테스트하기
    프로그래밍/기타 2024. 9. 9. 17:01
    반응형

     

    테스트 피라미드

    어떤 종류의 테스트를 목표로 해야 하는가?

    - 만드는 비용이 적고, 유지보수하기 쉽고, 빨리 실행되고, 안정적인 작은 크기의 테스트들에 대해 높은 커버리지를 유지해야 한다.

    - 하나의 '단위'가 제대로 동작하는지 확인할 수 있는 단위 테스트

     

    여러개의 단위와 단위를 넘는 경계, 아키텍처 경계, 시스템 경계를 결합하는 테스트는 만드는 비용이 더 비쌈, 실행이 더 느림!

     

    테스트가 비싸질수록 테스트의 커버리지 목표는 낮게 잡아야함!

    그렇지 않으면 신규 기능 개발 < 테스트 작성에 더 많은 시간을 쏟아붓게 된다.

     

    단위 테스트, 통합 테스트, 시스템 테스트의 정의는 프로젝트마다 다른 의미를 가질 수 있다.

     

    단위 테스트

    피라미드의 토대

    하나의 클래스를 인스턴스화하고 해당 클래스의 인터페이스를 통해 기능들을 테스트한다.

    만약 테스트 중인 클래스가 다른 클래스에 의존한다면 -> 모킹처리

     

    통합 테스트

    연결됫 여러 유닛을 인스턴스화한다.

    시작점이 되는 클래스의 인터페이스로 데이터를 보낸 후 유닛들의 네트워크가 기대한대로 잘 동작하는지 검증한다.

     

    시스템 테스트

    애플리케이션을 구성하는 모든 객체 네트워크를 가동시켜 특정 유스케이스가 전 계층에서 잘 동작하는지 검증한다.

     

    단위 테스트로 도메인 엔티티 테스트하기

    class AccountTest {
    	@Test
    	void withdrawalSucceeds () {
    		AccountId accountId = new AccountId(1L); Account account = defaultAccount
    			.withAccountId(accountId)
    			.withBaselineBalance (Money.of (555L)) 
                •withActivityWindow(new ActivityWindow(
    				defaultActivity() 
                   	 	.withTargetAccount(accountId)
    					.withMoney (Money.of (999L))
                        .build(), 
               		defaultActivity()
    					.withTargetAccount(accountId) 
                        .withMoney(Money.of(IL)) 
                        .build()))
    			.build();
    		
            boolean success = account.withdraw(Money.of(555L), new AccountId (99L));
    		
            assertThat(success).isTrue ();
    		assertThat (account.getActivityWindow().getActivities()).hasSize(3); 
            assertThat(account.calculateBalance()).isEqualTo(Money.of (1000L));
       }
    }

     

    withdraw() 메서드가 기대한대로 동작하는지 검증한다.

    특정 상태의 Account를 인스턴스화하고 withdraw() 메서드를 호출해서 출금을 성공했는지, 검증하고, Account 객체의 상태에 대해 기대되는 부수효과들이 잘 일어났는지 확인하는 단순한 단위 테스트다.

    이런 식의 단위 테스트가 도메인 엔티티에 녹아 있는 비즈니스 규칙을 검증하기 가장 적절하다!

     

    단위 테스트로 유스케이스 테스트하기

    SendMoneyService 테스트

    - SendMoney 유스케이스

    출금 계좌의 잔고가 다른 트랜잭션에 의해 변경되지 않도록 락(lock)을 건다 .

    출금 계좌에서 돈이 출금되고 나면 똑같이 입금 계좌에 락을 걸고 돈을 입금시킨다.

    그러고 나서 두 계좌에서 모두 락을 해제한다.

     

    참고 링크

     

    테스트 가독성을 높이기 위해 given / when / then 섹션으로 나눈다

     

    given

    출금 및 입금 Account의 인스턴스를 각각 생성하고 적절한 상태로 만든다.

    SendMoneyCommand의 인스턴스도 만들어서 유스케이스의 입력으로 사용했다

     

    when

    유스케이스를 실행하기 위해 sendMoney() 메소드를 호출했다

     

    then

    트랜잭션이 성공적이었는지 확인하고, AccountLock에 대해 특정 메서드가 호출됐는지 검증한다

     

    중요한 핵심만 골라 집중해서 테스트하는 것이 좋다!

     

    통합 테스트로 웹 어댑터 테스트하기

    JSON 문자열 등의 형태로 HTTP를 통해 입력을 받고, 입력에 대한 유효성 검증을 하고,

    유스케이스에서 사용할 수 있는 포맷으로 매핑하고, 유스케이스에 전달한다 -> 웹 어댑터의 역할

     

    웹 어댑터 테스트에서는 앞의 모든 단계들이 기대한대로 동작하는지 검증 필요

     

    class SendMoneyControllerTest {
    
    	@Autowired
    	private MockMvc mockMvc;
    
    	@MockBean
    	private SendMoneyUseCase sendMoneyUseCase;
    
    	@Test
    	void testSendMoney() throws Exception {
    
    		mockMvc.perform(post("/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}",
    				41L, 42L, 500)
    				.header("Content-Type", "application/json"))
    				.andExpect(status().isOk());
    
    		then(sendMoneyUseCase).should()
    				.sendMoney(eq(new SendMoneyCommand(
    						new AccountId(41L),
    						new AccountId(42L),
    						Money.of(500L))));
    	}
    
    }

     

     

    testSendMoney() 메서드에서는 입력 객체를 만들고 목 HTTP 요청을 웹 컨트롤러에 보낸다.

    요청 바디는 JSON 문장열 형태로 입력 객체를 포함한다.

    isOk() 메서드로 HTTP 응답의 상태가 200임을 검증한다 

     

    @WebWvcTest 애너테이션

    스프링이 특정 요청 경로, 자바와 JSON간의 매핑, HTTP 입력 검증 등에 필요한 전체 객체 네트워크를 인스턴스화 하도록 만든다.

    그리고 테스트에서는 웹 컨트롤러가 이 네트워크의 일부로서 잘 동작하는지 검증한다.

    웹 컨트롤러가 스프링 프레임워크에 강하게 묶여있기 때문에 격리된 상태로 테스트하기 보다 통합된 상태로 테스트하는 것이 합리적이다!

    통합 테스트로 영속성 어댑터 테스트하기

    비슷한 이유로 영속성 어댑터의 테스트에는 단위 테스트보다는 통합 테스트를 적용하는 것이 합리적이다.

    단순히 어댑터의 로직만 검증하고 싶은게 아니라 데이터 베이스 매핑도 검증하고 싶기 때문이다.

     

    @DataJpaTest
    @Import({AccountPersistenceAdapter.class, AccountMapper.class})
    class AccountPersistenceAdapterTest {
    
    	@Autowired
    	private AccountPersistenceAdapter adapterUnderTest;
    
    	@Autowired
    	private ActivityRepository activityRepository;
    
    	@Test
    	@Sql("AccountPersistenceAdapterTest.sql")
    	void loadsAccount() {
    		Account account = adapterUnderTest.loadAccount(new AccountId(1L), LocalDateTime.of(2018, 8, 10, 0, 0));
    
    		assertThat(account.getActivityWindow().getActivities()).hasSize(2);
    		assertThat(account.calculateBalance()).isEqualTo(Money.of(500));
    	}
    
    	@Test
    	void updatesActivities() {
    		Account account = defaultAccount()
    				.withBaselineBalance(Money.of(555L))
    				.withActivityWindow(new ActivityWindow(
    						defaultActivity()
    								.withId(null)
    								.withMoney(Money.of(1L)).build()))
    				.build();
    
    		adapterUnderTest.updateActivities(account);
    
    		assertThat(activityRepository.count()).isEqualTo(1);
    
    		ActivityJpaEntity savedActivity = activityRepository.findAll().get(0);
    		assertThat(savedActivity.getAmount()).isEqualTo(1L);
    	}
    
    }

     

    @DataJpaTest 어노테이션으로 스프링 데이터 리포지토리들을 포함해서 데이터베이스 접근에 필요한 객체 네트워크를 인스턴스화 해야한다고 스프링에 알려준다.

    @Import 어노테이션을 추가해서 특정 객체가 이 네트워크에 추가됐다는 것을 명확하게 표현한다.

     

    loadAccount() 메서드에 대한 테스트

    SQL 스크립트를 이용해 데이터베이스를 특정 상태로 만듦

    어댑터 API를 이용해 계좌를 가져온 후 SQL 스크립트에서 설정한 상태값을 가지고 있는지 검증

     

    updateActivities() 메서드에 대한 테스트

    새로운 계좌 활동을 가진 Account 객체를 만들어서 저장하기 위해 어댑터로 전달

    ActivityRepository의 API를 이용해 이 활동이 데이터베이스에 잘 저장됐는지 확인

    DB를 모킹하지 않았다는 점이 중요! 

    모킹했더라도 높은 커버리지를 보여줬을테지만 실제 DB와 연동시 에러 발생의 가능성!

    실제 DB를 대상으로 테스트시 인메모리 DB 등 다른 DB 시스템을 신경쓰지 않아도 되는 장점

     

    시스템 테스트로 주요 경로 테스트하기

    시스템 테스트

    전체 애플리케이션을 띄우고 API를 통해 요청을 보내고 모든 계층이 조화롭게 잘 동작하는지 검증한다.

    TestRestTemplate을 이용해서 요청을 보낸다.

    테스트를 프로덕션 환경에 조금 더 가깝게 만들기 위해 실제 HTTP 통신을 한다.

     

    얼마만큼의 테스트가 충분할까?

    라인 커버리지는 테스트 성공을 측정하는 데 있어서는 잘못된 지표다

    -> 코드의 중요한 부분이 전혀 커버되지 않을 수 있다!

     

    얼마나 마음 편하게 소프트웨어를 배포할 수 있는간가 중요.

    테스트를 실행한 후에 소프트웨어를 배포해도 될 만큼 테스트를 신뢰한다면 그걸로 됐다!

     

    프로덕션의 버그를 수정하고 이로부터 배우는 것

    - 테스트가 왜 이 버그를 잡지 못했을까? -> 이 케이스를 커버할 수 있는 테스트 추가

     

    육각형 아키텍처에서 사용하는 전략

    - 도메인 엔티티를 구현할 때는 단위 테스트로 커버하자

    - 유스케이스를 구현할 때는 단위 테스트로 커버하자

    - 어댑터를 구현할 때는 통합 테스트로 커버하자

    - 사용자가 취할 수 있는 중요 애플리케이션 경로는 시스템 테스트로 커버하자

     

    유지보수가 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?

    육각형 아키텍처는 도메인 로직과 바깥으로 향한 어댑터를 깔끔하게 분리한다

    핵심 도메인 로직 > 단위테스트, 어댑터 > 통합테스트로 처리하는 명확한 테스트 전략 정의 가능

     

    반응형

    댓글

Designed by Tistory.