如何使用 ASP.NET Core 对外部 API 进行集成测试

时间:2021-01-24 03:16:50

标签: c# asp.net-core integration-testing xunit

我正在尝试对外部 API 进行一些集成测试。我在网上找到的大多数指南都是关于测试 ASP.NET web api,但关于外部 API 的内容并不多。我想在这个 API 上测试一个 GET 请求,并通过检查状态代码是否正常来确认它是否通过。但是这个测试没有通过,我想知道我是否正确地做这件事。目前它给我一个状态代码 404(未找到)。

我将 xUnitMicrosoft.AspNetCore.TestHost 一起使用,您建议我如何测试外部 API?

private readonly HttpClient _client;

public DevicesApiTests()
{
    var server = new TestServer(new WebHostBuilder()
        .UseEnvironment("Development")
        .UseStartup<Startup>());
    _client = server.CreateClient();
}

[Theory]
[InlineData("GET")]
public async Task GetAllDevicesFromPRTG(string method)
{
    //Arrange
    var request = new HttpRequestMessage(new HttpMethod(method), "https://prtg.nl/api/content=Group,Device,Status");

    //Act
    var response = await _client.SendAsync(request);

    // Assert
    response.EnsureSuccessStatusCode();
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

编辑

我尝试测试的 API 调用如下所示,并且工作正常

private readonly DbContext _dbContext;
private readonly IDevicesRepository _devicesRepository;

public DevicesAPIController(DbContext dbContext, IDevicesRepository devicesRepository)
{
    _dbContext = dbContext;
    _devicesRepository = devicesRepository;
}

[HttpPost("PostLiveDevicesToDatabase")]
public async Task<IActionResult> PostLiveDevicesToDatabase()
{
    try
    {

        using (var httpClient = new HttpClient())
        {
            httpClient.DefaultRequestHeaders.Clear();
            httpClient.DefaultRequestHeaders.Accept.Add(
                new MediaTypeWithQualityHeaderValue("application/json"));

            using (var response = await httpClient
                .GetAsync(
                    "https://prtg.nl/api/content=Group,Device,Status")
            )
            {
                string apiResponse = await response.Content.ReadAsStringAsync();
                var dataDeserialized = JsonConvert.DeserializeObject<Devices>(apiResponse);

                devicesList.AddRange(dataDeserialized.devices);

                foreach (DevicesData device in devicesList)
                {

                    _dbContext.Devices.Add(device);
                    devicesAdded.Add(device);

                    _dbContext.SaveChanges();
                }
            }
        }
    }
    catch
    {
        return BadRequest();
    }
}

2 个答案:

答案 0 :(得分:1)

测试服务器的基址是localhost。 TestServer 用于内存集成测试。通过 TestServer.CreateClient() 创建的客户端将创建一个 HttpClient 实例,该实例使用内部消息处理程序来管理特定于您的 API 的请求。

如果您尝试通过调用测试服务器来访问外部 URL。您将按照设计获得 404。

如果 https://prtg.nl/api/content 不是您的 API 的本地链接,而是您要访问的实际外部链接,则使用独立的 HttpClient

//...

private static readonly HttpClient _client;

static DevicesApiTests() {
    _client = new HttpClient();
}

[Theory]
[InlineData("GET")]
public async Task GetAllDevicesFromPRTG(string method) {
    //Arrange
    var request = new HttpRequestMessage(new HttpMethod(method), "https://prtg.nl/api/content=Group,Device,Status");

    //Act
    var response = await _client.SendAsync(request);

    // Assert
    response.EnsureSuccessStatusCode();
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

//...

如果这意味着通过您的 api 端到端,那么您需要调用依赖于目标控制器和操作的本地 API 端点

答案 1 :(得分:1)

我想提出一种替代解决方案,其中涉及更改要测试的代码的设计。

当前显示的测试用例与外部 API 耦合并测试其响应 200 OK 而不是您的代码的能力(即,您的代码根本没有被引用)。这也意味着,如果无法建立到服务器的连接(例如,可能是 CI/CD 管道中的隔离构建代理,或者只是一个不稳定的咖啡馆 WIFI),则测试会由于断言之外的其他原因而失败。

我建议将 HttpClient 及其特定于 API 的配置提取到抽象中,就像您对 IDevicesRepository 所做的一样(尽管示例中未使用它)。这允许您替换来自 API 的响应并仅测试您的代码。替换可以探索边缘情况,例如连接断开、空响应、格式错误的响应、外部服务器错误等。这样您就可以在代码中使用更多的故障路径,并使测试与外部 API 分离。

抽象的实际替换将在测试的“安排”阶段完成。您可以为此使用 Moq NuGet 包。

更新

要提供使用 Moq 模拟空 API 响应的示例,请考虑一个假设的抽象,例如:

public interface IDeviceLoader
{
    public IEnumerable<DeviceDto> Get();
}

public class DeviceDto
{
    // Properties here...
}

请记住,示例抽象不是异步的,当您调用 I/O(即网络)时,这可以被视为最佳实践。我跳过它以保持简单。有关如何处理异步方法的信息,请参阅 Moq documentation

为了模拟响应,测试用例的主体可以是:

[Fact]
public async Task CheckEndpointHandlesEmptyApiResponse()
{
    // How you get access to the database context and device repository is up to you.
    var dbContext = ...
    var deviceRepository = ...

    //Arrange
    var apiMock = new Mock<IDeviceLoader>();

    apiMock.Setup(loader => loader.Get()).Returns(Enumerable.Empty<DeviceDto>());

    var controller = new DevicesAPIController(dbContext, deviceRepository, apiMock.Object);

    //Act
    var actionResponse = controller.PostLiveDevicesToDatabase();

    // Assert
    // Check the expected HTTP result here...
}

请查看其存储库(上面链接)中的 Moq 文档以获取更多示例。