如何在集成测试中模拟服务调用?
How to mock out call to service in an integration test?
假设我想对 API 控制器方法执行集成测试,如下所示:
public async Task<IActionResult> Get(Guid id)
{
try
{
if (id == Guid.Empty)
{
return new BadRequestObjectResult("Id is not valid");
}
var result = await _fileStorageService.GetFileUrlWithAccessKey(id);
if (result == null)
{
return new NotFoundObjectResult("Could not find any file with given id");
}
var document = new Document()
{
Url = result
};
return Ok(document);
}
catch (StorageException storageException)
{
switch (storageException.RequestInformation.HttpStatusCode)
{
case 404:
return Json(StatusCode(404));
default:
return Json(StatusCode(500));
}
}
catch (Exception)
{
return Json(StatusCode(500));
}
}
我的集成测试是这样的(刚开始实现,第一个测试还没有完全完成):
public class DocumentsControllerTest : IClassFixture<TestServerFixture>
{
private readonly HttpClient Client;
public DocumentsControllerTest(TestServerFixture fixture)
{
Client = fixture.Client;
}
[Fact]
public async Task Get_WhenCalledNotExistingFileId_ShouldReturn404StatusCode()
{
var nonExistentId = Guid.NewGuid();
var response = await Client.GetAsync($"/documents/{nonExistentId}");
}
}
在 API 控制器方法中,我想模拟对 _fileStorageService.GetFileUrlWithAccessKey(id);
的调用
我试图通过模拟接口 IFileStorageService
来模拟对 __fileStorageService 的调用
public class TestServerFixture
{
/// <summary>
/// Test fixture that can be used by test classes where we want an HttpClient
/// that can be shared across all tests in that class.
/// </summary>
public HttpClient Client { get; set; }
private readonly TestServer _server;
public TestServerFixture()
{
var webHostBuilder = new WebHostBuilder()
.UseEnvironment("UnitTest")
.UseStartup<Startup>()
.ConfigureServices(services =>
{
services.TryAddScoped(serviceProvider => A.Fake<IFileStorageService>());
});
_server = new TestServer(webHostBuilder);
Client = _server.CreateClient();
}
public void Dispose()
{
Client.Dispose();
_server.Dispose();
}
}
但我不认为在我的 TestServerFixture
class 中对 var result = await _fileStorageService.GetFileUrlWithAccessKey(id);
的调用被模拟出来是正确的,因为我的测试代码一直进入这段代码并且我收到错误是因为我没有向 fileStorageService 提供参数。在这种情况下我该怎么做才能完全模拟对服务的调用,这样我们就不会进入该代码?
仅仅因为您创建了一个虚假服务并不意味着您模拟框架知道要return 调用什么方法。
我对 FakeItEasy 不太熟悉,但我想你想要这样的东西:
var fakeFileStorageService = A.Fake<IFileStorageService>();
A.CallTo(() => fakeFileStorageService.GetFileUrlWithAccessKey(A<Guid>.Ignored))
.Returns(fakeResult);
其中 fakeResult 是您想要 return 用于测试的结果。
在项目负责人的帮助下我发现:
在TestServerFixture
这句话可以换成:
.ConfigureServices(services =>
{
services.TryAddScoped(serviceProvider => A.Fake<IFileStorageService>());
});
有:
.ConfigureServices(services =>
{
services.AddScoped(serviceProvider => A.Fake<IFileStorageService>());
});
为了让 mock 发挥作用,您需要在启动时更改您的 ConfigureServices 方法 class。
您无需调用 AddScoped、AddInstance、Add 或 AddTransient,而是调用要在测试中替换的 class 的 TryAdd... 变体。 (来源:https://fizzylogic.nl/2016/07/22/running-integration-tests-for-asp-net-core-apps/)
所以这意味着,在我的例子中,startup.cs class 将需要像这样调用 TryAddScoped 而不是 AddScoped。
services.TryAddScoped<IFileStorageService, AzureBlobStorageService>();
在我的情况下,我已经尝试了services.TryAddScoped(...)
但无法解决,然后我改为services.AddScoped(...)
并且有效。
.ConfigureServices(services =>
{
services.AddScoped(serviceProvider => A.Fake<IFileStorageService>());
});
顺便说一句,谢谢,您的解决方案有效
假设我想对 API 控制器方法执行集成测试,如下所示:
public async Task<IActionResult> Get(Guid id)
{
try
{
if (id == Guid.Empty)
{
return new BadRequestObjectResult("Id is not valid");
}
var result = await _fileStorageService.GetFileUrlWithAccessKey(id);
if (result == null)
{
return new NotFoundObjectResult("Could not find any file with given id");
}
var document = new Document()
{
Url = result
};
return Ok(document);
}
catch (StorageException storageException)
{
switch (storageException.RequestInformation.HttpStatusCode)
{
case 404:
return Json(StatusCode(404));
default:
return Json(StatusCode(500));
}
}
catch (Exception)
{
return Json(StatusCode(500));
}
}
我的集成测试是这样的(刚开始实现,第一个测试还没有完全完成):
public class DocumentsControllerTest : IClassFixture<TestServerFixture>
{
private readonly HttpClient Client;
public DocumentsControllerTest(TestServerFixture fixture)
{
Client = fixture.Client;
}
[Fact]
public async Task Get_WhenCalledNotExistingFileId_ShouldReturn404StatusCode()
{
var nonExistentId = Guid.NewGuid();
var response = await Client.GetAsync($"/documents/{nonExistentId}");
}
}
在 API 控制器方法中,我想模拟对 _fileStorageService.GetFileUrlWithAccessKey(id);
我试图通过模拟接口 IFileStorageService
来模拟对 __fileStorageService 的调用public class TestServerFixture
{
/// <summary>
/// Test fixture that can be used by test classes where we want an HttpClient
/// that can be shared across all tests in that class.
/// </summary>
public HttpClient Client { get; set; }
private readonly TestServer _server;
public TestServerFixture()
{
var webHostBuilder = new WebHostBuilder()
.UseEnvironment("UnitTest")
.UseStartup<Startup>()
.ConfigureServices(services =>
{
services.TryAddScoped(serviceProvider => A.Fake<IFileStorageService>());
});
_server = new TestServer(webHostBuilder);
Client = _server.CreateClient();
}
public void Dispose()
{
Client.Dispose();
_server.Dispose();
}
}
但我不认为在我的 TestServerFixture
class 中对 var result = await _fileStorageService.GetFileUrlWithAccessKey(id);
的调用被模拟出来是正确的,因为我的测试代码一直进入这段代码并且我收到错误是因为我没有向 fileStorageService 提供参数。在这种情况下我该怎么做才能完全模拟对服务的调用,这样我们就不会进入该代码?
仅仅因为您创建了一个虚假服务并不意味着您模拟框架知道要return 调用什么方法。
我对 FakeItEasy 不太熟悉,但我想你想要这样的东西:
var fakeFileStorageService = A.Fake<IFileStorageService>();
A.CallTo(() => fakeFileStorageService.GetFileUrlWithAccessKey(A<Guid>.Ignored))
.Returns(fakeResult);
其中 fakeResult 是您想要 return 用于测试的结果。
在项目负责人的帮助下我发现:
在TestServerFixture
这句话可以换成:
.ConfigureServices(services =>
{
services.TryAddScoped(serviceProvider => A.Fake<IFileStorageService>());
});
有:
.ConfigureServices(services =>
{
services.AddScoped(serviceProvider => A.Fake<IFileStorageService>());
});
为了让 mock 发挥作用,您需要在启动时更改您的 ConfigureServices 方法 class。 您无需调用 AddScoped、AddInstance、Add 或 AddTransient,而是调用要在测试中替换的 class 的 TryAdd... 变体。 (来源:https://fizzylogic.nl/2016/07/22/running-integration-tests-for-asp-net-core-apps/)
所以这意味着,在我的例子中,startup.cs class 将需要像这样调用 TryAddScoped 而不是 AddScoped。
services.TryAddScoped<IFileStorageService, AzureBlobStorageService>();
在我的情况下,我已经尝试了services.TryAddScoped(...)
但无法解决,然后我改为services.AddScoped(...)
并且有效。
.ConfigureServices(services =>
{
services.AddScoped(serviceProvider => A.Fake<IFileStorageService>());
});
顺便说一句,谢谢,您的解决方案有效