ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 단위 테스트 정리 6 - 통합 테스트
    책책책 책을 읽읍시다/프로그래밍 2022. 8. 3. 10:39

    실제로 통합 테스트는 대부분 시스템이 프로세스 외부 의존성과 통합해 어떻게 작동하는지를 검증한다. 다시 말해, 이 테스트는 컨트롤러 사분면에 속하는 코드를 다룬다. 단위 테스트는 도메인 모델을 다루는 반면, 통합 테스트는 프로세스 외부 의존성과 도메인 모델을 연결한는 코드를 확인한다.

    컨트롤러 사분면을 다루는 테스트가 단위 테스트일 수도 있다. 모든 프로세스 외부 의존성을 목으로 대체하면 테스트 간에 공유하는 의존성이 없어지므로 테스트 속도가 빨라지고 서로 격리될 수 있다. 그러나 대부분의 애플리케이션은 목으로 대체할 수 없는 프로세스 외부 의존성이 있다.

     

    단위 트세트로 가능 한 많이 비즈니스 시나리오의 예외 상황을 확인하고, 통합 테스트는 주요 흐름(happy path)과 단위 테스트가 다루지 못하는 기타 예외 상황(edge case)를 다룬다.

    어떤 외부 의존성을 직접 테스트해야 하는가?

     

    모든 프로세스 외부 의존성은 두 가지 범주로 나뉜다.

    • 관리 의존성(전체를 제어할 수 있는 프로세스 외부 의존성): 이러한 의존성은 애플리케이션을 통해서만 접근할 수 있으며, 해당 의존성과의 상호 작용은 외부 환경에서 볼 수 없다. 대표적인 예로 데이터베이스가 있다. 외부 시스템은 보통 데이터베이스에 직접 접근하지 않고 애플리케이션에서 제공하는 API를 통해 접근한다.
    • 비관리 의존성(전체를 제어할 수 없는 프로세스 외부 의존성) : 해당 의존성과의 상호 작용을 외부에서 볼 수 있다. 예를 들어 SMTP 서버와 메시지 버스 등이 있다. 둘 다 다른 애플리케이션에서 볼 수 있는 부작용을 발생시킨다.

     

    비관리 의존성에 대한 통신 패턴을 유지해야 하는 것은 하위 호환성을 지켜야 하기 때문이다. 이 작업에는 목이 제격이다. 목을 사용하면 모든 가능한 리팩터링을 고려해서 통신 패턴 영속성을 보장할 수 있다.

    그러나 관리 의존성과 통신하는 것은 애플리케이션뿐이므로 하위 호환성을 유지할 필요가 없다. 외부 클라이언트는 데이터베이스를 어떻게 구성하는지 신경 쓰지 않는다. 중요한 것은 시스템의 최종 상태다. 통합 테스트에서 관리 의존성의 실제 인스턴스를 사용하면 외부 클라이언트 관점에서 최종 상태를 확인할 수 있다. 또한 컬럼 이름을 변경하거나 데이터베이스를 이관하는 등 데이터페이스 리팩터링에도 도움이 된다.

     

    통합테스트에서 데이터베이스를 그대로 테스트할 수 없으면 통합 테스트를 아예 작성하지 말고 도메인 모델의 단위 테스트에만 집중하라. 항상 모든 테스트를 철저히 검토해야 한다. 가치가 충분하지 않은 테스트는 테스트 스위트에 있어서는 안된다.

    public class UserController
    {
       private readonly Database _database = new Database();
       private readonly MessageBus PmessageBus = new MessageBus();
     
       public string ChangeEmail(int userId, string newEmail)
       {
           object[] userData = _database.GetUserById(userId);
           User user = UserFactory.Create(userData);
     
           string error = user.CanChangeEmail();
           if (error != null)
               return error;
     
           object[] companyData = _database.GetCompany();
           Company company = CompanyFactory.Create(companyData);
     
           user.ChangeEmail(newEmail, company);
     
           _database.SaveCompany(company);
           _database.SaveUser(user);
           foreach (EmailChangedEvent ev in user.EmailChangedEvnets)
           {
               _messageBus.SendEmailChangedMessage(ev.UserId, ev.NewEmail);
           }
           return "OK";
       }
    }

    통합 테스트에 대한 일반적인 지침은 가장 긴 주요 흐름과 단위 테스트로는 수행할 수 없는 모든 예외 상황을 다루는 것이다. 가장 긴 주요 흐름은 모든 프로세스 외부 의존성을 거치는 것이다.

     

    CRM 프로젝트에서 가장 긴 주요 흐름은 기업 이메일에서 일반 이메일로 변경하는 것이다. 이 변경으로 인해 가장 부작용이 많다.

    • 데이터베이스에서 사용자와 회사 모두 업데이트된다. 즉 사용자는 유형을 (기업에서 일반으로) 변경하고 이메일도 변경하며, 회사는 직원 수를 변경한다.
    • 메세지 버스로 메세지를 보낸다.
    [Fact]
    public void Changing_email_from_corporate_to_non_corporate()
    {
       // given
       var db = new Database(ConnectionString); // 데이터베이스 저장소
       User user = CreateUser("user@mycorp.com", UserType.Employee, db);
       CreateCompany("mycorp.com", 1, db);
     
       var messageBusMock = new Mock<ImessageBus>();
       var sut = new UserController(db, messageBusMock.Object);
     
       // when
       string result = sut.ChangeEmail(user.UserId, "new@gmail.com");
     
       // then
       Assert.Equal("OK", result);
     
       //사용자 상태 검증
       object[] userData = db.GetUserById(user.UserId);
       User userFromDb = UserFactory.Create(userData);
       Assert.Equal("new@gmail.com", userFromDb.Email);
       Assert.Equal(UserType.Customer, userFromDb.Type);
     
       // 회사 상태 검증
       object[] companyData = db.GetCompany();
       Company companyFromDb = CompanyFactory.Create(companyData);
       Assert.Equal(0, companyFromDb.NumberOfEmployees);
     
       // 목 상호작용 확인
       messageBusMock.Verify( x => x.SendEmailChangedMessage(user.UserId, "new@gmail.com"), Times.Once);
    }

    입력 매개변수로 사용한 데이터와 별개로 데이터베이스 상태를 확인하는 것이 중요하다. 이를 위해 통합 테스트는 검증 구절에서 사용자와 회사 데이터를 각각 조회하고, 새로운 userFromDb와 companyFromDb 인스턴스를 생성한 후에 해당 상태를 검증만 한다. 이 방법을 사용하면 테스트가 데이터베이스에 대해 읽기와 쓰기를 모두 수행하므로 회귀 방지를 최대로 얻을 수 있다. 읽기는 컨트롤러에서 내부적으로 사용하는 동일한 코드를 써서 구현해야 한다.(이 예제에서는 Database, UserFactory, CompanyFactory 클래스 사용)

    헬퍼 메서드를 사용해 검증 구절 크기를 줄일 수 있고, messageBusMock은 회귀 방지가 그다지 좋지 않아 개선할 여지가 더 있지만, 일단 여기까지^^

     

    통합 테스트 모범 사례


    3가지 일반적인 지침이 있다.

    • 도메인 모델 경계 명시하기 : 도메인 모델을 코드베이스에서 명시적이고 잘 알려지 위치에 두도록 하라. 도메인 모델은 프로젝트가 해결하고자 하는 문제에 대한 도메인 지식의 모음이다. 도메인 모델에 명시적 경계를 지정하면 코드의 해당 부분을 더 잘 보여주고 더 잘 설명할 수 있다.
    • 애플리케이션 내 계층 줄이기 : 애플리케이션에 추상 계층이 너무 많으면 코드베이스를 탐색하기 어렵고 아주 간단한 연산이라 해도 숨은 로직을 이해하기가 너무 어려워진다. 단순히 직면한 문제의 구체적인 해결 방법을 알고 싶을 뿐이지, 외부와 단절된 채로 해결책을 일반화하려는 것은 아니다. 간접 계층을 코드를 추론하는 데 부정적인 영향을 미친다. 모든 기능이 각각의 계층으로 전개되면 모든 조각을 하나의 그림으로 만드는데 상당한 노력이 필요하다. 가능한 간접 계층을 적게 사용하라. 대부분의 백엔드 시스템에서는 도메인 모델, 애플리케이션 서비스 계층(컨트롤러), 인프라 계층, 이 세 가지만 활용하면 된다.
    • 순환 의존성 제거하기 : 대표적인 예는 콜백(callback)이다. 값 객체를 도입해 순환을 없애고, 호출부에 주는 결과를 값 객체로 반환하라. 순환 의존성은 코드를 읽고 이해하려고 할 때 알아야할 것이 많아서 큰 부담이 된다. 순환 의존성이 있으면 해결책을 찾기 위한 출발점이 명확하지 않기 때문이다. 하나의 클래스를 이해하려면 주변 클래스 그래프 전체를 한 번에 읽고 이해해야 하며, 심지어 소규모의 독립된 클래스조차도 파악하기가 어려워질 수 있다. 또한 순환 의존성은 테스트를 방해한다. 클래스 그래프를 나눠서 동작 단위를 하나 분리하려면 인터페이스에 의존해 목으로 처리해야 하는 경우가 많으며, 이는 도메인 모델을 테스트할 때 해서는 안된다. 

     

    목 사용 지침


    목은 비관리 의존성에만 해당하며 컨트롤러만 이러한 의존성을 처리하는 코드이기 때문에 통합 테스트에서 컨트롤러를 테스트할 때만 목을 적용해야 한다. 다음 3가지 지침이 있다.

    • 테스트당 목이 하나일 필요는 없음 : 동작 단위를 검증하는 데 필요한 목의 수는 관계가 없다. 
    • 호출 횟수 검증 하기 : 예상하는 호출이 있는가? 예상치 못한 호출은 없는가? 두 가지 모두 확인하는 것이 중요하다. 이 요구 사항은 다시 비관리 의존성과 하위 호환성을 지켜야 하는 데서 비롯된다. 호환성은 양방향이어야 한다. 즉, 애플리케이션은 외부 시스템이 예상하는 메시지를 생략해서는 안 되며 예상치 못한 메시지도 생성해서는 안 된다. 
    • 보유 타입만 목으로 처리하기 : 비관리 의존성에 접근하는 서드파티 라이브러리 위에 어댑터를 작성하라. 기본 타입 대신 해당 어댑터를 목으로 처리하라.

     

     

     

     

     

     

     

     

     

     

    댓글

Designed by Tistory.