65.9K
CodeProject 正在变化。 阅读更多。
Home

集成测试:比 AutoFixture 更多的 Fixture

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2020年5月13日

CPOL

4分钟阅读

viewsIcon

10399

解释 Fixture 的概念以及它如何用于单元测试和集成测试。

引言

正如在AutoFixture中使用的 Fixture 一样,它已被证明是测试自动化中非常有用的概念。Fixture 不仅对单元测试有用,而且这个概念在集成测试中也是可重复使用的。本文解释了 Fixture 是什么以及如何在单元测试和集成测试中使用这个概念。Fixture 总有一个共同点:它们为你管理测试的“准备”部分,这对各种测试都有用,而不仅仅是单元测试。

背景

如果你有一些 .NET Core 应用程序 TDD 的经验,包括模拟,最好是使用本文中使用的 .NET Core 3.1,这将非常有帮助。拥有 Fixture 的经验也很有用,但不是必需的。本文中显示的示例基于 xUnit,但这些概念和技术也可以应用于其他单元测试框架。

Using the Code

首先,我们来看看我们要测试的代码。代码如下:

[Route("api/[controller]")]
[ApiController]
public class SearchEngineController : ControllerBase
{
   private readonly ISearchEngineService _searchEngineService;

   public SearchEngineController(ISearchEngineService searchEngineService)
   {
       _searchEngineService = searchEngineService;
   }

   [HttpGet("{queryEntry}", Name = "GetNumberOfCharacters")]
   public async Task<ActionResult<int>> GetNumberOfCharacters(string queryEntry)
   {
       var numberOfCharacters = 
           await _searchEngineService.GetNumberOfCharactersFromSearchQuery(queryEntry);
       return Ok(numberOfCharacters);
   }
}

从上面的代码可以清楚地看出,它只是一个调用一些接口方法并以 OK 状态码 (200) 返回结果的控制器方法。以下是该接口的实现。

public class SearchEngineService : ISearchEngineService
{
   private readonly HttpClient _httpClient;

   public SearchEngineService(HttpClient httpClient)
   {
       _httpClient = httpClient;
   }

   public async Task<int> GetNumberOfCharactersFromSearchQuery(string toSearchFor)
   {
       var result = await _httpClient.GetAsync($"/search?q={toSearchFor}");
       var content = await result.Content.ReadAsStringAsync();
       return content.Length;
   }
}

这段代码也很简单。它执行一个网络请求并返回结果的长度。

以下是如何在Startup类中添加依赖项的。

public void ConfigureServices(IServiceCollection services)
{
   services.AddControllers();
   var googleLocation = Configuration["Google"];
   services.AddHttpClient<ISearchEngineService, SearchEngineService>(c =>
            c.BaseAddress = new Uri(googleLocation))
            .SetHandlerLifetime(TimeSpan.FromMinutes(5))
            .AddPolicyHandler(GetRetryPolicy());
}

现在可以清楚地看到,我们需要模拟两个依赖项

  • **内部依赖项:**这是注入到控制器的ISearchEngineService实例。
  • **外部依赖项:**这是作为添加的HttpClient的基地址设置的搜索引擎(URL)。对于集成测试,我们需要模拟它,因为我们无法控制它是否给出相同的响应以及是否保持在线。

内部依赖项的模拟通常用于单元测试目的,使用 Moq,如这篇文章中所述。外部依赖项的模拟通常用于集成测试目的,使用 WireMock,如这篇文章中所述,这里介绍的一些代码就来自这篇文章。这里显示的所有代码也都在GitHub上可用。

使用 Fixture 的单元测试如下所示

[Fact]
public async Task GetTest()
{
   // arrange
   var fixture = new Fixture().Customize(new AutoMoqCustomization());
   var service = fixture.Freeze<Mock<ISearchEngineService>>();
   service.Setup(a => a.GetNumberOfCharactersFromSearchQuery(It.IsNotNull<string>()))
                .ReturnsAsync(8);
   var controller = fixture.Build<SearchEngineController>().OmitAutoProperties().Create();

   // act
   var response = await controller.GetNumberOfCharacters("Hoi");

   // assert
   Assert.Equal(8, ((OkObjectResult)response.Result).Value);
   service.Verify(s => s.GetNumberOfCharactersFromSearchQuery("Hoi"), Times.Once);
}

在这里,我们看到以下内容

  1. 第一步是创建 Fixture。
  2. 之后,调用Freeze方法(在Create方法之前)来指定我们想要在测试结束时对其进行一些验证的**内部**依赖项。
  3. 需要使用Setup方法设置**内部**依赖项。
  4. 在测试方法的断言部分,我们验证对**内部**依赖项(service实例)的**调用**。

实现这样的测试需要一个 NuGet 包:AutoFixture.AutoMoq

自从 .NET Core 的最新版本以来,集成测试变得容易多了。与单元测试的主要区别在于集成测试更真实(因此可配置性更低)。Startup类和Program类都被包含在内。使用真实的内部依赖项而不是模拟/存根。只有外部依赖项需要模拟,因为这些依赖项可能脱机、行为变化或不稳定,而所有这些都不应该破坏测试。以下是使用 Fixture 进行集成测试的方法

[Fact]
public async Task GetTest()
{
   // arrange
   using (var fixture = new Fixture<Startup>())
   {
      using (var mockServer = fixture.FreezeServer("Google"))
      {
          SetupStableServer(mockServer, "Response");
          var controller = fixture.Create<SearchEngineController>();

          // act
          var response = await controller.GetNumberOfCharacters("Hoi");

          // assert
          var request = mockServer.LogEntries.Select(a => a.RequestMessage).Single();
          Assert.Contains("Hoi", request.RawQuery);
          Assert.Equal(8, ((OkObjectResult)response.Result).Value);
       }
    }
}

private void SetupStableServer(FluentMockServer fluentMockServer, string response)
{
    fluentMockServer.Given(Request.Create().UsingGet())
    .RespondWith(Response.Create().WithBody(response, encoding: Encoding.UTF8)
    .WithStatusCode(HttpStatusCode.OK));
}

在这里,我们看到类似的内容

  1. 第一步是使用Startup类创建 Fixture,以便在调用Create方法后初始化所有依赖项。
  2. 之后,调用FreezeServer方法(在Create方法之前)来设置我们想要在测试结束时对其进行一些验证的**外部**依赖项。
  3. 需要使用自编写的SetupStableServer方法设置内部依赖项,该方法描述了特定请求需要特定响应。这将在此处更详细地解释。
  4. 在测试方法的断言部分,我们验证发送到**外部**依赖项(模拟服务)的**网络请求**。

实现这样的测试需要另一个 NuGet 包:ConnectingApps.IntegrationFixture

从上面的代码可以清楚地看出,使用 Fixture 进行集成测试与使用 Fixture 进行单元测试基本上是相同的。但是,需要用外部依赖项来安排和验证外部依赖项,而不是内部依赖项。这听起来很明显,但是当在不使用 Fixture 的情况下设置集成测试时,需要很多样板代码,如此处所述。

关注点

在我学习 Fixture、使用 Fixture 和撰写本文时,我清楚地认识到 Fixture 的概念可以以多种方式使用。AutoFixture 以其在编写单元测试的准备部分时的实用性而闻名,但 Fixture 也可用于其他类型的测试(的准备部分)。它可以帮助我们开发者节省编写大量样板代码的工作。

历史

  • 2020年5月13日:初始版本
© . All rights reserved.