시뮬레이션을 검증하는 더 나은 방법(XUnit, Moq,.NET)

내가 TDD를 연습하기 시작했을 때, 단원 테스트를 작성하는 것은 나의 일상적인 일이 되었다.제 직장 생활에서 저는 몇 가지 기술을 배웠습니다. 이런 기술은 테스트를 더욱 명확하게 작성하고 모든 코드의 의존성을 검증하는 데 도움이 됩니다.XUnit 테스트 프레임워크를 사용하여 테스트를 작성하고 코드 종속성 시뮬레이션을 위해 Moq Nuget 패키지를 사용합니다.대부분의 개발자들이 좋은 시뮬레이션 검증 기술에 익숙하지 않다는 것을 알아차렸기 때문에 본고에서 소개하겠습니다.
본고에서 저는 단원 테스트의 장점, 예를 들어 더 좋은 코드 품질, 실행 가능한 문서, 복잡한 업무 장면을 신속하게 집행하기 쉽다는 것을 소개하지 않겠습니다.나는 대부분의 상황에서 단원 테스트가 유익하고 심지어 강제적이라는 것을 발견했다.

예제 코드


단원 테스트를 시작하려면 코드가 필요합니다.나는 하나의 방법으로 간단한 클래스를 만들었다.클래스의 방법은 새로운 순서를 만드는 논리를 포함합니다.
public class OrderService
{
    private readonly IItemRepository _itemRepository;
    private readonly IOrderRepository _orderRepository;

    public OrderService(
        IItemRepository itemRepository, 
        IOrderRepository orderRepository)
    {
        _itemRepository = itemRepository;
        _orderRepository = orderRepository;
    }

    public async Task CreateAsync(int itemId, int itemQuantity)
    {
        var item = await _itemRepository.GetAsync(itemId);
        if (item.Stock < itemQuantity) 
            throw new Exception($"Item id=[{itemId}] has not enough stock for order.");

        Order order = new()
        {
            ItemId = item.Id,
            Quantity = itemQuantity
        };

        await _orderRepository.CreateAsync(order);
    }
}
데이터베이스에서 데이터를 검색한다고 가정하십시오.이러한 종속 관계는 저장소로 추상화됩니다.추상적인 의존 관계는 단원 테스트에서의 시뮬레이션에 필수적이다.

테스트 코드


나는 즐거움 경로 단원 테스트부터 시작하는 것을 좋아한다.우선 테스트 샘플 코드부터 시작하겠습니다.
public class OrderServiceTests
{
    private readonly OrderService _sut;
    private readonly Mock<IItemRepository> _itemRepositoryMock;
    private readonly Mock<IOrderRepository> _orderRepositoryMock;

    public OrderServiceTests()
    {
        _itemRepositoryMock = new Mock<IItemRepository>();
        _orderRepositoryMock = new Mock<IOrderRepository>();
        _sut = new OrderService(
            _itemRepositoryMock.Object, 
            _orderRepositoryMock.Object);
    }

    [Fact]
    public void CreateAsync_ShouldCreateNewOrder()
    {
        Assert.False(true);
    }
}
만약 코드 구축에 성공했고, 우리가 실패한 테스트 결과를 얻었다면, 우리는 모두 준비가 되었다.다음 단계는 즐거움 경로 단원 테스트를 실현하는 것이다.
[Fact]
    public async Task CreateAsync_ShouldCreateNewOrder()
    {
        const int itemId = 1;
        const int quantity = 2;
        const int existingItemStock = 3;

        _itemRepositoryMock.Setup(m => m.GetAsync(It.Is<int>(i => i == itemId)))
            .ReturnsAsync(() => new Item
            {
                Id = itemId,
                Stock = existingItemStock
            });

        await _sut.CreateAsync(itemId, quantity);

        _itemRepositoryMock.Verify(m => m.GetAsync(itemId));
    }
이 테스트를 분석해 봅시다.나는 모든 단원 테스트에서 AAA(배열, 동작, 단언) 모드를 사용한다.이것은 업계 표준으로 테스트를 구축하는 깨끗한 방법이다.
  • '배열'부분에서 사용 중인 파라미터를 지정하고 호출 후 아날로그 방법의 결과를 설정합니다.
  • "act"부분에서 우리는 테스트 중인 방법이라고 부른다.
  • "단언"부분에서 설정된 아날로그 호출을 검증합니다.나는 단언을 사용하지 않았다. 왜냐하면 나의 방법은 값을 되돌려 주지 않기 때문이다.
  • 비록 이 테스트는 통과되었지만, 그것은 문제가 하나 있다.확인하지 않았습니다await _orderRepository.CreateAsync(order);.나는 _orderRepositry 시뮬레이션을 설정하지 않았다.Moq의 기본 시뮬레이션 동작으로 인해 발생합니다.시뮬레이션을 만들 때 비헤이비어가 기본값으로 설정됩니다.이 작업을 수행하는 방법은 Moq의 소스 코드에서 확인할 수 있습니다.
    /// <summary>
    ///   Initializes an instance of the mock with <see cref="MockBehavior.Default"/> behavior.
    /// </summary>
    /// <example>
    ///   <code>
    ///     var mock = new Mock<IFormatProvider>;();
    ///   </code>
    /// </example>
    public Mock(): this(MockBehavior.Default)
    {
    }
    
    MockBehavior는 생성된 시뮬레이션 동작을 지정하는 열거입니다.사용 가능한 가치와 동작은 다음과 같습니다.

  • Strict: 일치하는 구성이 없는 경우 메서드나 속성을 호출할 때마다 예외가 발생합니다.

  • 느슨함: Moq는 모든 호출을 받아들이고 유효한 반환 값을 만들려고 시도합니다.

  • 기본값: Loose와 같습니다.
  • 기본적으로, Moq는 모든 예상 호출을 강제로 설명하지 않고 단원 테스트를 만들 수 있습니다.이것은 테스트 개발을 가속화시킬 수 있지만, 나는 이것이 나쁜 방법이라고 생각한다.개발자는 반드시 자신의 코드와 코드의 매번 변경에 대해 책임을 져야 한다.이것은 엄격하게 설정MockBehavior을 통해 확보할 수 있다.이것은 아날로그 호출을 설정해야 한다.
     public OrderServiceTests()
        {
            _itemRepositoryMock = new Mock<IItemRepository>(MockBehavior.Strict);
            _orderRepositoryMock = new Mock<IOrderRepository>(MockBehavior.Strict);
            _sut = new OrderService(_itemRepositoryMock.Object, _orderRepositoryMock.Object);
        }
    
    아날로그 호출을 설정하지 않으면 다음 메시지가 표시됩니다.
    Moq.MockException
    IOrderRepository.CreateAsync(Order) invocation failed with mock behavior Strict.
    All invocations on the mock must have a corresponding setup.
    ....
    
    테스트를 복구하기 위해서 IOrderRepository의 설정을 추가해야 합니다.
    [Fact]
        public async Task CreateAsync_ShouldCreateNewOrder()
        {
            const int itemId = 1;
            const int quantity = 2;
            const int existingItemStock = 3;
    
            _itemRepositoryMock.Setup(m => m.GetAsync(It.Is<int>(i => i == itemId)))
                .ReturnsAsync(() => new Item
                {
                    Id = itemId,
                    Stock = existingItemStock
                });
    
            _orderRepositoryMock.Setup(m => m.CreateAsync(It.IsAny<Order>())).Returns(Task.CompletedTask);
    
            await _sut.CreateAsync(itemId, quantity);
    
            _itemRepositoryMock.Verify(m => m.GetAsync(itemId));
            _orderRepositoryMock.Verify(m => m.CreateAsync(It.IsAny<Order>()));
        }
    
    이 복구 후에 더 많은 검증 코드를 도입하고 있음을 보실 수 있습니다.설정이 여러 개 있으면 모든 검증을 실행해야 합니다.만약 코드가 더욱 복잡하고 여러 가지 방법이 호출된다면, 이것은 복잡성과 피할 수 있는 자질구레한 코드를 가져올 것이다.이것은 시뮬레이션 실례 MockRepository 클래스를 만들고 관리하는 데 도움을 줄 수 있다.이 종류는 시뮬레이션을 한 곳에서 만들고 검증하는 데 도움을 줍니다.
    public class OrderServiceTests
    {
        private readonly OrderService _sut;
        private readonly MockRepository _mockRepository;
        private readonly Mock<IItemRepository> _itemRepositoryMock;
        private readonly Mock<IOrderRepository> _orderRepositoryMock;
    
        public OrderServiceTests()
        {
            _mockRepository = new MockRepository(MockBehavior.Strict);
            _itemRepositoryMock = _mockRepository.Create<IItemRepository>();
            _orderRepositoryMock = _mockRepository.Create<IOrderRepository>();
            _sut = new OrderService(_itemRepositoryMock.Object, _orderRepositoryMock.Object);
        }
    
    MockRepository를 사용하면 우리는 같은 시뮬레이션 행위를 설정했고 VerifyAll() 방법으로 모든 호출을 검증할 수 있다.
     [Fact]
        public async Task CreateAsync_ShouldCreateNewOrder()
        {
            //test code
    
            //_itemRepositoryMock.Verify(m => m.GetAsync(itemId));
            //_orderRepositoryMock.Verify(m => m.CreateAsync(It.IsAny<Order>()));
            _mockRepository.VerifyAll();
        }
    
    더욱 진일보하기 위해서 우리는 Dispose() 방법을 사용하여 시뮬레이션을 검증할 수 있다.Dispose() XUnit를 사용하여 모든 테스트 후에 실행하기 때문에 우리는 설정된 모든 시뮬레이션을 검증할 수 있고 모든 테스트 방법에서 _mockRepository.VerifyAll() 코드 줄을 피할 수 있다.
    public class OrderServiceTests : IDisposable
    {
        private readonly OrderService _sut;
        private readonly MockRepository _mockRepository;
        private readonly Mock<IItemRepository> _itemRepositoryMock;
        private readonly Mock<IOrderRepository> _orderRepositoryMock;
    
        public OrderServiceTests(){...}
    
        [Fact]
        public async Task CreateAsync_ShouldCreateNewOrder(){...}
    
        public void Dispose()
        {
            _mockRepository.VerifyAll();
        }
    }
    
    이렇게 해서, 우리는 모크를 검증하고 테스트 코드를 깔끔하게 유지하는 간결한 방법을 가지게 되었다.
    전체 코드here를 찾을 수 있습니다.
    최저 주문량에 대한 더 많은 정보는 방문해 주십시오here.

    좋은 웹페이지 즐겨찾기