使用 Moq 的 ElasticClient 的代码覆盖率
Code coverage for ElasticClient using Moq
我正在尝试通过单元测试分析代码覆盖率,我目前正在使用 Moq 库执行单元测试,不知何故我走错了路,想知道以下场景是否适用于使用 Moq
下面是一段代码
public interface ISearchWorker
{
void DeleteIndex(string indexName);
T GetClient<T>();
}
public class ElasticSearchWorker : ISearchWorker
{
public void DeleteIndex(string indexName)
{
IElasticClient elasticClient = GetClient<IElasticClient>();
if (elasticClient.IndexExists(indexName).Exists)
{
_ = elasticClient.DeleteIndex(indexName);
}
}
public T GetClient<T>()
{
string nodeList = "http://localhost:9200/";
List<Node> nodes = nodeList.Split(',').Select(uri => new Node(new Uri(uri))).ToList();
IConnectionPool sniffingConnectionPool = new SniffingConnectionPool(nodes);
IConnectionSettingsValues connectionSettings = new ConnectionSettings(sniffingConnectionPool);
return (T)(IElasticClient)new ElasticClient(connectionSettings);
}
}
下面是单元测试的代码片段
[TestClass]
public class SearchTestClass
{
private ISearchWorker searchWorker;
private Mock<ISearchWorker> searchWorkerMoq;
private readonly string indexName = "testIndex";
[TestInitialize]
public void SetupElasticClient()
{
searchWorkerMoq = new Mock<ISearchWorker>();
var elasticClient = new Mock<IElasticClient>();
searchWorkerMoq.Setup(c => c.GetClient<IElasticClient>()).Returns(elasticClient.Object).Verifiable();
searchWorker = searchWorkerMoq.Object;
}
[TestMethod]
public void DeleteIndexTest()
{
try
{
searchWorker.DeleteIndex(indexName);
searchWorkerMoq.Verify(c => c.GetClient<IElasticClient>(), Times.Once());
}
catch (System.Exception)
{
throw;
}
}
}
行
searchWorkerMoq.Verify(c => c.GetClient<IElasticClient>(), Times.Once());
抛出以下异常
(Moq.MockException: '
Expected invocation on the mock once, but was 0 times: c => c.GetClient<IElasticClient>())
通读大多数与 Moq 相关的信息,这似乎不是执行 Moq 测试的合适方法,IElasticClient 对象应该从外部提供给 ElasticSearchWorker class
不通过外部注入提供 IElasticClient 对象的原因是我们计划为另一个搜索提供程序(Azure 搜索)实现 ISearchWorker,因此希望将客户端实体包含在实现 ISearchWorker 的 class 中界面
想知道是否有更好的方法来执行此测试,以及我们如何实现此场景的代码覆盖率。
所以我假设您明白为什么这不起作用,您只是要求 "a good way" 解决这个问题。
现在我并不是说这是最好的方法,但它可能是最快的,同时仍然是 "clean"。
应用接口隔离("I" 来自 SOLID)。制作两个接口而不是一个接口,然后在稍后阶段实现它们。
// Don't have a C# IDE with me, so sorry if I leave some syntax errors.
public interface ISearchClientProvider
{
T GetClient<T>();
}
public interface ISearchWorker
{
void DeleteIndex(string indexName);
}
public class ElasticSearchWorker : ISearchWorker{
private readonly ISearchClientProvider _clientProvider;
public ElasticSearchWorker(ISearchClientProvider clientProvider){
_clientProvider = clientProvider;
}
public void DeleteIndex(string indexName)
{
var elasticClient = _clientProvider.GetClient<IElasticClient>();
if (elasticClient.IndexExists(indexName).Exists)
{
_ = elasticClient.DeleteIndex(indexName);
}
}
}
public class ElasticSearchClientProvider : ISearchClientProvider{/*some implementation*/}
public class AzureSearchWorker : ISearchWorker{/*some implementation*/}
public class AzureSearchClientProvider : ISearchClientProvider{/*some implementation*/}
那么测试代码应该是这样的:
// would actually prefer to name it ElasticSearchWorkerTests
[TestClass]
public class SearchTestClass
{
private readonly ElasticSearchWorker _searchWorker;
private readonly ISearchClientProvider _elasticClientProvider;
private readonly string indexName = "testIndex";
// would prefer to name it SetupElasticSearchWorker
[TestInitialize]
public void SetupElasticClient()
{
var elasticClient = new Mock<IElasticClient>();
// Setup for IElasticClient.IndexExists() function:
// I don't know what is the return type of IndexExists,
// so I am assuming here that it is some dynamic Object
elasticClient.Setup(c => c.IndexExists(indexName)).Returns(new {Exists = true});
// Setup for IElasticCleint.DeleteIndex might also be necessary here.
_elasticClientProvider = new Mock<ISearchClientProvider>();
_elasticClientProvider.Setup(c => c.GetClient<IElasticClient>()).Returns(elasticClient.Object).Verifiable();
_searchWorker = new ElasticSearchWorker(_elasticClientProvider);
}
// would prefer to name it DeleteIndexTest_GetsSearchClient,
// because the function does more than is checked here, e.g., Checks index, deletes index.
[TestMethod]
public void DeleteIndexTest()
{
try
{
searchWorker.DeleteIndex(indexName);
searchWorkerMoq.Verify(c => c.GetClient<IElasticClient>(), Times.Once());
}
catch (System.Exception)
{
throw;
}
}
}
这样本次测试就没有http请求了。
一般来说(有争议的部分)如果您希望您的代码更易于单元测试:
- 使用Ports and Adapters(六边形,或者你想怎么称呼它)架构
- 经验法则。不要在其他 public 方法中调用 public 方法。使用 composition 来解决这个问题。我在这个例子中使用了合成。
- 经验法则。不要在 class 中创建具有行为的对象。让你的 composition root 创建它们并将它们作为依赖项注入到你的 class 中,一个接口的实现。如果无法在你的组合根中创建它们(也许你只在 运行 时间知道你需要什么样的对象),那么使用工厂模式。
- 免责声明。您不需要 DI 容器即可使用组合根模式。你可以开始 with poor man's DI.
- 总会有 class 不可单元测试的,因为来自第 3 方的某些依赖项将不可模拟。您的目标是使那些 classes very small 然后对它们进行一些集成测试。 (我相信,测试覆盖率工具也会使用)
- 免责声明。我看到有人通过 HttpClientHandler 替换成功地模拟了 HttpClient。但我还没有看到成功的 EF db 上下文模拟 - 它们在某些时候都失败了。
- 注意。人们普遍认为你应该 not mock interfaces you do not own。所以,从这个意义上说,我上面的解决方案也不干净,将来可能会给你带来麻烦。
我正在尝试通过单元测试分析代码覆盖率,我目前正在使用 Moq 库执行单元测试,不知何故我走错了路,想知道以下场景是否适用于使用 Moq
下面是一段代码
public interface ISearchWorker
{
void DeleteIndex(string indexName);
T GetClient<T>();
}
public class ElasticSearchWorker : ISearchWorker
{
public void DeleteIndex(string indexName)
{
IElasticClient elasticClient = GetClient<IElasticClient>();
if (elasticClient.IndexExists(indexName).Exists)
{
_ = elasticClient.DeleteIndex(indexName);
}
}
public T GetClient<T>()
{
string nodeList = "http://localhost:9200/";
List<Node> nodes = nodeList.Split(',').Select(uri => new Node(new Uri(uri))).ToList();
IConnectionPool sniffingConnectionPool = new SniffingConnectionPool(nodes);
IConnectionSettingsValues connectionSettings = new ConnectionSettings(sniffingConnectionPool);
return (T)(IElasticClient)new ElasticClient(connectionSettings);
}
}
下面是单元测试的代码片段
[TestClass]
public class SearchTestClass
{
private ISearchWorker searchWorker;
private Mock<ISearchWorker> searchWorkerMoq;
private readonly string indexName = "testIndex";
[TestInitialize]
public void SetupElasticClient()
{
searchWorkerMoq = new Mock<ISearchWorker>();
var elasticClient = new Mock<IElasticClient>();
searchWorkerMoq.Setup(c => c.GetClient<IElasticClient>()).Returns(elasticClient.Object).Verifiable();
searchWorker = searchWorkerMoq.Object;
}
[TestMethod]
public void DeleteIndexTest()
{
try
{
searchWorker.DeleteIndex(indexName);
searchWorkerMoq.Verify(c => c.GetClient<IElasticClient>(), Times.Once());
}
catch (System.Exception)
{
throw;
}
}
}
行
searchWorkerMoq.Verify(c => c.GetClient<IElasticClient>(), Times.Once());
抛出以下异常
(Moq.MockException: '
Expected invocation on the mock once, but was 0 times: c => c.GetClient<IElasticClient>())
通读大多数与 Moq 相关的信息,这似乎不是执行 Moq 测试的合适方法,IElasticClient 对象应该从外部提供给 ElasticSearchWorker class
不通过外部注入提供 IElasticClient 对象的原因是我们计划为另一个搜索提供程序(Azure 搜索)实现 ISearchWorker,因此希望将客户端实体包含在实现 ISearchWorker 的 class 中界面
想知道是否有更好的方法来执行此测试,以及我们如何实现此场景的代码覆盖率。
所以我假设您明白为什么这不起作用,您只是要求 "a good way" 解决这个问题。
现在我并不是说这是最好的方法,但它可能是最快的,同时仍然是 "clean"。
应用接口隔离("I" 来自 SOLID)。制作两个接口而不是一个接口,然后在稍后阶段实现它们。
// Don't have a C# IDE with me, so sorry if I leave some syntax errors.
public interface ISearchClientProvider
{
T GetClient<T>();
}
public interface ISearchWorker
{
void DeleteIndex(string indexName);
}
public class ElasticSearchWorker : ISearchWorker{
private readonly ISearchClientProvider _clientProvider;
public ElasticSearchWorker(ISearchClientProvider clientProvider){
_clientProvider = clientProvider;
}
public void DeleteIndex(string indexName)
{
var elasticClient = _clientProvider.GetClient<IElasticClient>();
if (elasticClient.IndexExists(indexName).Exists)
{
_ = elasticClient.DeleteIndex(indexName);
}
}
}
public class ElasticSearchClientProvider : ISearchClientProvider{/*some implementation*/}
public class AzureSearchWorker : ISearchWorker{/*some implementation*/}
public class AzureSearchClientProvider : ISearchClientProvider{/*some implementation*/}
那么测试代码应该是这样的:
// would actually prefer to name it ElasticSearchWorkerTests
[TestClass]
public class SearchTestClass
{
private readonly ElasticSearchWorker _searchWorker;
private readonly ISearchClientProvider _elasticClientProvider;
private readonly string indexName = "testIndex";
// would prefer to name it SetupElasticSearchWorker
[TestInitialize]
public void SetupElasticClient()
{
var elasticClient = new Mock<IElasticClient>();
// Setup for IElasticClient.IndexExists() function:
// I don't know what is the return type of IndexExists,
// so I am assuming here that it is some dynamic Object
elasticClient.Setup(c => c.IndexExists(indexName)).Returns(new {Exists = true});
// Setup for IElasticCleint.DeleteIndex might also be necessary here.
_elasticClientProvider = new Mock<ISearchClientProvider>();
_elasticClientProvider.Setup(c => c.GetClient<IElasticClient>()).Returns(elasticClient.Object).Verifiable();
_searchWorker = new ElasticSearchWorker(_elasticClientProvider);
}
// would prefer to name it DeleteIndexTest_GetsSearchClient,
// because the function does more than is checked here, e.g., Checks index, deletes index.
[TestMethod]
public void DeleteIndexTest()
{
try
{
searchWorker.DeleteIndex(indexName);
searchWorkerMoq.Verify(c => c.GetClient<IElasticClient>(), Times.Once());
}
catch (System.Exception)
{
throw;
}
}
}
这样本次测试就没有http请求了。
一般来说(有争议的部分)如果您希望您的代码更易于单元测试:
- 使用Ports and Adapters(六边形,或者你想怎么称呼它)架构
- 经验法则。不要在其他 public 方法中调用 public 方法。使用 composition 来解决这个问题。我在这个例子中使用了合成。
- 经验法则。不要在 class 中创建具有行为的对象。让你的 composition root 创建它们并将它们作为依赖项注入到你的 class 中,一个接口的实现。如果无法在你的组合根中创建它们(也许你只在 运行 时间知道你需要什么样的对象),那么使用工厂模式。
- 免责声明。您不需要 DI 容器即可使用组合根模式。你可以开始 with poor man's DI.
- 总会有 class 不可单元测试的,因为来自第 3 方的某些依赖项将不可模拟。您的目标是使那些 classes very small 然后对它们进行一些集成测试。 (我相信,测试覆盖率工具也会使用)
- 免责声明。我看到有人通过 HttpClientHandler 替换成功地模拟了 HttpClient。但我还没有看到成功的 EF db 上下文模拟 - 它们在某些时候都失败了。
- 注意。人们普遍认为你应该 not mock interfaces you do not own。所以,从这个意义上说,我上面的解决方案也不干净,将来可能会给你带来麻烦。