// Arrange var db = new Database(ConnectionString); User user = CreateUser("user@mycorp.com", UserType.Employee, db); CreateCompany("mycorp.com", 1, db); // Sets up the mocks var messageBusMock = new Mock<IMessageBus>(); var loggerMock = new Mock<IDomainLogger>(); var sut = new UserController(db, messageBusMock.Object, loggerMock.Object);
// Act string result = sut.ChangeEmail(user.UserId, "new@gmail.com");
// Arrange var db = new Database(ConnectionString); User user = CreateUser("user@mycorp.com", UserType.Employee, db); CreateCompany("mycorp.com", 1, db); // Sets up the mocks var busMock = new Mock<IBus>(); var messageBus = new MessageBus(busMock.Object); // Uses a concrete class instead of the interface var loggerMock = new Mock<IDomainLogger>(); var sut = new UserController(db, messageBus, loggerMock.Object);
// Act string result = sut.ChangeEmail(user.UserId, "new@gmail.com");
object[] companyData = db.GetCompany(); Company companyFromDb = CompanyFactory.Create(companyData); Assert.Equal(0, companyFromDb.NumberOfEmployees); // Verifies the actual message sent to the bus busMock.Verify( x => x.Send( "Type: USER EMAIL CHANED; " + $"Id: {user.UserId}; " + "NewEmail: new@gmail.com",) Times.Once ); loggerMock.Verify( x => x.UserTypeHasChanged(user.UserId,UserType.Employee,UserType.Customer), Times.Once ); }
이제 IMessageBus 인터페이스가 아닌 messageBus 객체를 통해 Mocking 외에 용도가 없던 IMessageBus 인터페이스를 소거했음을 알 수 있다.
또한 메시지 버스로의 전송 방식도 변경되었다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// AS-IS - Verifies the interactions with the mocks messageBusMock.Verify( x => x.SendEmailChangedMessage(user.UserId, "new@gmail.com"), Times.Once );
// TO-BE - Verifies the actual message sent to the bus busMock.Verify( x => x.Send( "Type: USER EMAIL CHANED; " + $"Id: {user.UserId}; " + "NewEmail: new@gmail.com",) Times.Once );
기존에 사용자 정의 클래스에 대한 호출을 검증하는 것과, 외부 시스템에 전송한 실제 텍스트가 다른 것을 볼 수 있다.
외부 시스템은 애플리케이션으로부터 텍스트 메시지를 수신하고 MessageBus와 같은 클래스를 호출하지 않기 때문에, 시스템의 바깥에서 식별할 수 있는 유일한 부작용으로 볼 수 있다.
결과적으로 시스템의 끝에서 상호 작용을 확인함으로서 회귀 방지와 리팩토링 내성이 향상되는 것이다.
이후 리팩토링을 수행하더라도 메시지 구조를 유지하는 한 해당 테스트는 성공할 것이며, 단위 테스트에 비해 통합 테스트와 종단간 테스트가 리팩토링 내성이 우수한 것과 같은 매커니즘을 지니게 된다.
9.1.2. Mock을 Spy로 대체하기
원제 : Replacing mocks with spies
Spy는 Mock처럼 동일한 목적을 수행하는 테스트 대역이다.
단지 프레임워크의 도움을 받아 생성하는 Mock과 달리 직접 작성해야한다는 차이점이 있을 뿐이다.
시스템의 끝에 있는 클래스의 경우는 Mock보다는 Spy가 더 나은 대안이 되기도 한다.
Spy는 검증 단계에서 코드를 재사용함으로써, 테스트의 크기를 줄이고 가독성을 향상 시킨다.
publicclassBusSpy : IBus { // Stores all sent messages locally private List<string> _sentMessages = new List<string>(); publicvoidSend(string message) { _sentMessages.Add(message); }
public BusSpy ShouldSendNumberOfMessages(int number) { Assert.Equal(number, _sentMessages.Count); returnthis; }
// Asserts that the message has been sent public BusSpy WithEmailChangedMessage(int userId, string newEmail) { string message = "Type: USER EMAIL CHANGED; " + $"Id: {userId}; " + $"NewEmail: {newEmail}";
Assert.Contains( _sentMessages, x => x == message); returnthis; } }
// Arrange var db = new Database(ConnectionString); User user = CreateUser("user@mycorp.com", UserType.Employee, db); CreateCompany("mycorp.com", 1, db);
// Sets up the spy and mocks var busSpy = new BusSpy(); var messageBus = new MessageBus(busSpy); var loggerMock = new Mock<IDomainLogger>(); var sut = new UserController(db, messageBus, loggerMock.Object);
// Act string result = sut.ChangeEmail(user.UserId, "new@gmail.com");
object[] companyData = db.GetCompany(); Company companyFromDb = CompanyFactory.Create(companyData); Assert.Equal(0, companyFromDb.NumberOfEmployees); // Verifies the actual message sent to the bus busSpy.ShouldSendNumberOfMessages(1) .WithEmailChangedMessage(user.UserId, "new@gmail.com"); loggerMock.Verify( x => x.UserTypeHasChanged(user.UserId,UserType.Employee,UserType.Customer), Times.Once ); }
BusSpy에서 제공하는 플루언트 인터페이스를 통해 메시지 버스와의 상호 작용을 검증하는 코드의 간결함과 가독성을 확보할 수 있게되었다.
헌데, 기존의 IMessageBus와 별다른 차이점이 없어보인다.
이는 겉모습만 그런 것이고 BusSpy는 테스트 코드 영역에, MessageBus는 제품 코드 영역에 속한다는 차이점이 있다.
테스트에서 검증 구절을 작성할 때 제품 코드에 의존하지 않아야함을 생각하면 매우 큰 차이임을 알 수 있다.
9.2. Mock 처리에 대한 모범 사례
원제 : Mocking best practices
지금까지 Mock을 처리하는 모범 사례로 두 가지를 배웠다.
비관리 의존성에만 Mock 적용하기(Applying mocks to unmanaged dependencies only)
시스템 끝에 있는 의존성에 대해 상호 작용 검증 (Verifying the interactions with those dependencies at the very edges of your system)
이제 나머지 세 가지 모범 사례에 대해서 알아보자.
통합 테스트에서만 Mock을 사용하고 단위 테스트에서는 사용하지 않기(Using mocks in integration tests only, not in unit tests)
항상 Mock의 호출 수를 확인하기(Always verifying the number of calls made to the mock)
보유한 타입에 대해서만 Mock으로 처리하기(Mocking only types that you own)
9.2.1. Mock은 통합 테스트만을 위한 것이다.
원제 : Mocks are for integration tests only
세번째 사례인 통합 테스트에서만 Mock을 사용하고 단위 테스트에서는 사용하지 않기 에 대해서 알아보자.
이 지침을 지키는 것에는 복잡한 코드를 처리하는 도메인 모델과 통신을 처리하는 컨트롤러의 두 계층의 분화가 내재되어있다.
이는 도메인 모델 계층에 대한 테스트는 단위 테스트로 수행하고, 컨트롤러 계층은 통합 테스트를 수행해야 함을 뜻한다.
Mock은 비관리 의존성에 대해서만 대치해야 하며, 컨트롤러가 비관리 의존성을 처리하는 것임을 생각하면 된다.
9.2.2. 하나의 테스트가 하나의 Mock만 가질 필요는 없다.
원제 : Not just one mock per test
테스트를 작성하다보면 하나의 테스트는 하나의 Mock만 사용해야한다는 말을 들을 수도 있다.
이는 Mock이 두 개 이상인 경우 여러 검증을 하나의 테스트에서 수행할 수 있는 가능성이 있기 때문인데, 이는 “단위” 라는 용어에서 오는 오해이다.
단위 테스트의 “단위”는 코드의 단위가 아니라 동작의 단위임을 명심하자.
동작의 단위와 이를 구현하는 데 필요한 코드의 양은 관계가 없다.
Mock을 사용할 때도 동일한 원칙이 적용되므로, 하나의 동작을 검증하는 데 Mock의 수는 관계가 없다.
9.2.3. 호출 횟수 검증하기
원제 : Verifying the number of calls
비관리 의존성과의 통신에 대해서는 아래 두 가지 항목을 확인하는 것이 중요하다.
예상하는 호출이 있는가?(The existence of expected calls)
예상치 못한 호출은 없는가?(The absence of unexpected calls)
위 요구사항은 비관리 의존성과 하위 호환성을 지켜야하는 데서 비롯된다.
애플리케이션은 외부 시스템이 예상하는 메시지를 생략해선 안되며, 예상치 못한 메시지를 생성해서도 ㄷ안된다.
즉, 호환성은 양방향이어야 한다.
따라서 테스트 대상 시스템이 다음과 같이 메시지를 전송하는지 확인하는 것만으로는 충분하지 않다.
1 2 3
messageBusMock.Verify( x => x.SendEmailChangedMessage(user.UserId, "new@gmail.com") );
이 메시지가 정확히 한 번만 전송되는지도 검증해야 한다.
1 2 3 4
messageBusMock.Verify( x => x.SendEmailChangedMessage(user.UserId, "new@gmail.com"), Times.Once // HERE );