测试具有外部依赖项的操作方法

Testing an action method with external dependencies

这是 HomeController:

中 Action 方法的示例
[HttpPost]
public async Task<dynamic> UnitTest(string data)
{
    var httpClient = new HttpClient();
    var request = JsonConvert.SerializeObject(data);
    var url = "https://jsonplaceholder.typicode.com/posts";
    var response = await httpClient.PostAsync(url, new StringContent(request, Encoding.UTF8, "application/json"));
    string responseContent = await response.Content.ReadAsStringAsync();
    return responseContent;
}

我想测试它,但我不知道如何。我尝试了以下方法:

[TestMethod]
public async Task JsonRightTest()
{
    MyModelR model1 = new MyModelR
    {
        Title = "foo",
        Body = "bar",
        UserId = 1
    };
    string output1 = JsonConvert.SerializeObject(model1);
    var url = "Home/UnitTest";
    var response = await _client.PostAsync(url, new StringContent(output1, Encoding.UTF8, "application/json"));
    response.EnsureSuccessStatusCode();

    var responseContent = await response.Content.ReadAsStringAsync();
    var responseModel = JsonConvert.DeserializeObject<MyModel>(responseContent);


    // Assert
    Assert.AreEqual(1,
        responseModel.UserId);
}


internal class MyModel
{
    public string Title { get; set; }
    public string Body { get; set; }
    public int UserId { get; set; }
    public int Id { get; set; }
}

internal class MyModelR
{
    public string Title { get; set; }
    public string Body { get; set; }
    public int UserId { get; set; }
}

不幸的是,上面的方法不起作用。由于我很困惑,你能给我一些答案吗:

我想调用实际的外部端点。

API (https://jsonplaceholder.typicode.com/posts) 是在 Internet 上找到的,可用于测试目的。

这似乎是一个 XY problem 问题的混合体。

被测代码与实现问题紧密耦合,应该将该外部调用封装在可以在独立单元测试期间模拟的服务抽象之后。

应该遵循的一些重构步骤....

测试中正在构建的那些模型应该在行动中。

[HttpPost]
public async Task<IActionResult> UnitTest([FromBody]MyDataR data) {
    var httpClient = new HttpClient();
    var requestJson = JsonConvert.SerializeObject(data);
    var url = "https://jsonplaceholder.typicode.com/posts";
    var response = await httpClient.PostAsync(url, new StringContent(requestJson, Encoding.UTF8, "application/json"));
    if(response.IsSuccessStatusCode) {
        var responseContent = await response.Content.ReadAsStringAsync();
        var responseModel = JsonConvert.DeserializeObject<MyModel>(responseContent);        
        return Ok(responseModel);
    }else 
        return StatusCode(response.StatusCode);
}

进一步重构,外部端点的实际调用应该被抽象掉

public interface IExternalService {
    Task<MyModel> PostDataAsync(MyData data);
}

并相应实施

public class ExternalService : IExternalService  {
    // should consider abstracting this as well but that is another matter
    static Lazy<HttpClient> _httpClient = new Lazy<HttpClient>(() => new HttpClient());

    private HttpClient httpClient {
        get { return _httpClient.Value; }
    }       

    public async Task<MyModel> PostDataAsync(MyData data) {
        var requestJson = JsonConvert.SerializeObject(data);
        var url = "https://jsonplaceholder.typicode.com/posts";
        var content = new StringContent(requestJson, Encoding.UTF8, "application/json")
        var response = await httpClient.PostAsync(url, content);
        var responseContent = await response.Content.ReadAsStringAsync();
        if(response.IsSuccessStatusCode) {
            var responseContent = await response.Content.ReadAsStringAsync();
            var responseModel = JsonConvert.DeserializeObject<MyModel>(responseContent);        
            return responseModel;
        }else 
            return null;
    }
}

控制器中的操作现在看起来像

private readonly IExternalService externalService; // Assumed injected into the controller

[HttpPost]
public async Task<IActionResult> UnitTest([FromBody]MyDataR data) {
    var responseModel = await externalService.PostDataAsync(data);
    if(responseModel != null) {
        return Ok(responseModel);
    }else 
        return BadRequest();
}

通过消除与外部服务调用的紧密耦合,这将允许根据需要对控制器进行隔离测试,以验证其行为是否符合预期。

如果希望检查外部端点是否按预期运行,现在可以自行测试外部服务调用实现。这将被视为集成测试,因为它依赖于实际的外部端点。

[TestMethod]
public async Task JsonRightTest() {
    // Arrange
    var expected = 1;

    var model = new MyModelR {
        Title = "foo",
        Body = "bar",
        UserId = 1
    };

    var target = new ExternalService(); // System under test

    // Act
    var responseModel = await target.PostDataAsync(model);

    // Assert
    Assert.IsNotNull(responseModel);
    var actual = responseModel.UserId;
    Assert.AreEqual(expected, actual);
}

现在应该可以更轻松地检查外部服务以验证其行为是否符合预期。

在生产中,您将确保外部服务抽象及其实现在组合根中注册。

services.AddTransient<IExternalService, ExternalService>();

以便正确注入依赖控制器。