英文:
How to mock a class where a variable is initialized in constructor in C#?
问题
以下是您要的代码部分的中文翻译:
public class CustomerClient : ICustomerClient
{
private readonly ILoggingService _loggingService;
private readonly ILoggingServiceConfigProvider _loggingServiceConfigProvider;
private readonly string _customerEndpoint;
private readonly IHttpPolicy _policy;
private readonly IHttpClientWrapper _httpClientWrapper;
private const string LOGGING_TITLE = "service";
private const string SERVICE_NAME = "service";
public CustomerClient(string customerEndpoint, ILoggingService loggingService, IConfigurationManager configManager,
ILoggingServiceConfigProvider loggingServiceConfigProvider)
{
_customerEndpoint = customerEndpoint ?? throw new ArgumentNullException(nameof(cpsEndpoint));
_loggingService = loggingService ?? throw new ArgumentNullException(nameof(loggingService));
_loggingServiceConfigProvider = loggingServiceConfigProvider ??
throw new ArgumentNullException(nameof(loggingServiceConfigProvider));
_customerEndpoint = customerEndpoint;
_policy = new HttpPolicyBuilderAsync(configManager, _loggingService, "customerPolicyOptions.json", LOGGING_TITLE);
_httpClientWrapper = new HttpClientWrapper(_cpsEndpoint, loggingService, SERVICE_NAME, _loggingServiceConfigProvider);
}
public CustomerInfo GetData(int rosterId, IList<string> leagues, IList<string> teams, LoggingContext loggingContext)
{
//....
}
}
public class HttpPolicyBuilderAsync(IConfigurationManager configManager, ILoggingService loggingService, string fileName, string svcName) : base(configManager, loggingService, fileName)
{
_configurationStateManager = new ConfigurationStateManager<HttpPolicyContainer>(configManager, fileName, RebuildConfigObjects, loggingService);
_svcName = svcName ?? throw new ArgumentNullException(nameof(svcName));
}
public void EnsureValidStatusCode(HttpStatusCode statusCode)
{
if (_configurationStateManager.GetState().RetryHttpStatusCodes.Contains(statusCode))
{
throw new HttpRequestExceptionWithStatusCode($"Status code: {statusCode} to retry for config: {_fileName}", statusCode);
}
}
[SetUp]
public void Setup()
{
_loggingServiceMock = new Mock<ILoggingService>();
_configManagerMock = new Mock<IConfigurationManager>();
_loggingServiceConfigProviderMock = new Mock<ILoggingServiceConfigProvider>();
_httpClientWrapperMock = new Mock<IHttpClientWrapper>();
// 异常会在调用此处时抛出
_customerClient = new CustomerClient("endpoint", _loggingServiceMock.Object, _configManagerMock.Object, _loggingServiceConfigProviderMock.Object);
}
[Test]
public void CustomerClient_GetData_ShouldReturnEmptyRoster_WhenResponseStatusCodeIsNotOK()
{
....
}
这是您提供的代码的翻译。如果您需要更多帮助,请随时告诉我。
英文:
I have a below class which I am trying to write unit test for using Moq framework in C#.
public class CustomerClient : ICustomerClient
{
private readonly ILoggingService _loggingService;
private readonly ILoggingServiceConfigProvider _loggingServiceConfigProvider;
private readonly string _customerEndpoint;
private readonly IHttpPolicy _policy;
private readonly IHttpClientWrapper _httpClientWrapper;
private const string LOGGING_TITLE = "service";
private const string SERVICE_NAME = "service";
public CustomerClient(string customerEndpoint, ILoggingService loggingService, IConfigurationManager configManager,
ILoggingServiceConfigProvider loggingServiceConfigProvider)
{
_customerEndpoint = customerEndpoint ?? throw new ArgumentNullException(nameof(cpsEndpoint));
_loggingService = loggingService ?? throw new ArgumentNullException(nameof(loggingService));
_loggingServiceConfigProvider = loggingServiceConfigProvider ??
throw new ArgumentNullException(nameof(loggingServiceConfigProvider));
_customerEndpoint = customerEndpoint;
_policy = new HttpPolicyBuilderAsync(configManager, _loggingService, "customerPolicyOptions.json", LOGGING_TITLE);
_httpClientWrapper = new HttpClientWrapper(_cpsEndpoint, loggingService, SERVICE_NAME, _loggingServiceConfigProvider);
}
public CustomerInfo GetData(int rosterId, IList<string> leagues, IList<string> teams, LoggingContext loggingContext)
{
//....
}
}
And here is HttpPolicyBuilderAsync
constructor
public HttpPolicyBuilderAsync(IConfigurationManager configManager, ILoggingService loggingService, string fileName, string svcName) : base(configManager, loggingService, fileName)
{
_configurationStateManager = new ConfigurationStateManager<HttpPolicyContainer>(configManager, fileName, RebuildConfigObjects, loggingService);
_svcName = svcName ?? throw new ArgumentNullException(nameof(svcName));
}
public void EnsureValidStatusCode(HttpStatusCode statusCode)
{
if (_configurationStateManager.GetState().RetryHttpStatusCodes.Contains(statusCode))
{
throw new HttpRequestExceptionWithStatusCode($"Status code: {statusCode} to retry for config: {_fileName}", statusCode);
}
}
I came up with below code but when I am running the unit test, I am getting an exception because I have a _policy
in the constructor of CustomerClient
which is giving me an error since I am not mocking that as of now. How can I mock that so that when I call CustomerClient
constructor, it is already initialized for me.
[TestFixture]
public class CustomerClientTests
{
private Mock<ILoggingService> _loggingServiceMock;
private Mock<IConfigurationManager> _configManagerMock;
private Mock<ILoggingServiceConfigProvider> _loggingServiceConfigProviderMock;
private Mock<IHttpClientWrapper> _httpClientWrapperMock;
private CustomerClient _customerClient;
[SetUp]
public void Setup()
{
_loggingServiceMock = new Mock<ILoggingService>();
_configManagerMock = new Mock<IConfigurationManager>();
_loggingServiceConfigProviderMock = new Mock<ILoggingServiceConfigProvider>();
_httpClientWrapperMock = new Mock<IHttpClientWrapper>();
// exception is thrown once this is called
_customerClient = new CustomerClient("endpoint",_loggingServiceMock.Object, _configManagerMock.Object, _loggingServiceConfigProviderMock.Object);
}
[Test]
public void CustomerClient_GetData_ShouldReturnEmptyRoster_WhenResponseStatusCodeIsNotOK()
{
....
}
}
答案1
得分: 3
概要
问题根源在于您的类设计。
目前,您的 CustomerClient
类对 HttpPolicyBuilderAsync
和 HttpClientWrapper
有硬性依赖。在构造函数中实例化其他类的对象,特别是外部类,是一种反模式和不良实践,除非它们是没有任何功能的简单结构或记录。
这就是控制反转(IoC)容器的用途。对于C#,有许多可用的IoC容器,您可以使用它们来解析您的硬性依赖并从类中移除紧密耦合,比如 TinyIoC 和 SimpleInjector。
由于您无法更改 CustomerClient
构造函数的签名,正如您在其中一条评论中提到的那样,您可以使用IoC容器来获取依赖项。当然,这意味着您的类将依赖于IoC容器本身,这比依赖构造函数内部的实现要好得多,因为它消除了紧密耦合。
解决方案
为了解决您的问题,您可以使用IoC容器将提供程序注入到您的类中。在您的生产代码中,提供程序的实现将提供一个适当的 HttpPolicyBuilderAsync
实例。
IHttpPolicyProvider 接口
public interface IHttpPolicyProvider
{
IHttpPolicy GetPolicy(IConfigurationManager configManager, ILoggingService loggingService, string fileName, string svcName);
}
HttpPolicyBuilderProvider 实现
public class HttpPolicyBuilderProvider : IHttpPolicyProvider
{
public IHttpPolicy GetPolicy(IConfigurationManager configManager, ILoggingService loggingService, string fileName, string svcName)
{
return new HttpPolicyBuilderAsync(configManager, loggingService, fileName, svcName);
}
}
CustomerClient
使用 TinyIoC,您的类可以修改为如下所示:
using TinyIoC;
public class CustomerClient : ICustomerClient
{
private readonly ILoggingService _loggingService;
private readonly ILoggingServiceConfigProvider _loggingServiceConfigProvider;
private readonly string _customerEndpoint;
private readonly IHttpPolicy _policy;
private readonly IHttpClientWrapper _httpClientWrapper;
private const string LOGGING_TITLE = "service";
private const string SERVICE_NAME = "service";
public CustomerClient(string customerEndpoint, ILoggingService loggingService, IConfigurationManager configManager,
ILoggingServiceConfigProvider loggingServiceConfigProvider)
{
_customerEndpoint = customerEndpoint ?? throw new ArgumentNullException(nameof(cpsEndpoint));
_loggingService = loggingService ?? throw new ArgumentNullException(nameof(loggingService));
_loggingServiceConfigProvider = loggingServiceConfigProvider ??
throw new ArgumentNullException(nameof(loggingServiceConfigProvider));
_customerEndpoint = customerEndpoint;
_policy = TinyIoCContainer.Current.Resolve<IHttpPolicyProvider>().GetPolicy(configManager, _loggingService, "customerPolicyOptions.json", LOGGING_TITLE);
_httpClientWrapper = new HttpClientWrapper(_cpsEndpoint, loggingService, SERVICE_NAME, _loggingServiceConfigProvider);
}
//...
}
测试中的用法
在测试代码中,您可以创建一个返回 IHttpPolicy
模拟的 IHttpPolicyProvider
模拟,并在 TinyIoC 中注册提供程序:
using TinyIoC;
[TestFixture]
public class CustomerClientTests
{
private Mock<ILoggingService> _loggingServiceMock;
private Mock<IConfigurationManager> _configManagerMock;
private Mock<ILoggingServiceConfigProvider> _loggingServiceConfigProviderMock;
private Mock<IHttpClientWrapper> _httpClientWrapperMock;
private CustomerClient _customerClient;
private Mock<IHttpPolicyProvider> _httpPolicyProviderMock;
private Mock<IHttpPolicy> _httpPolicyMock;
[SetUp]
public void Setup()
{
_loggingServiceMock = new Mock<ILoggingService>();
_configManagerMock = new Mock<IConfigurationManager>();
_loggingServiceConfigProviderMock = new Mock<ILoggingServiceConfigProvider>();
_httpClientWrapperMock = new Mock<IHttpClientWrapper>();
_httpPolicyProviderMock = new Mock<IHttpPolicyProvider>();
_httpPolicyMock = new Mock<IHttpPolicy>();
_httpPolicyProviderMock.Setup(provider => provider.GetPolicy(It.IsAny<IConfigurationManager>(), It.IsAny<ILoggingService>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(_httpPolicyMock.Object);
TinyIoCContainer.Current.Register<IHttpPolicyProvider>(_httpPolicyProviderMock.Object);
_customerClient = new CustomerClient("endpoint", _loggingServiceMock.Object, _configManagerMock.Object, _loggingServiceConfigProviderMock.Object);
}
//...
}
生产中的用法
在生产代码中,您可以简单地使用 TinyIoC 注册 HttpPolicyBuilderProvider
:
using TinyIoC;
public class YourApp()
{
public YourApp()
{
TinyIoCContainer.Current.Register<IHttpPolicyProvider, HttpPolicyBuilderProvider>();
}
}
注意: 据我所知,您需要为 HttpClientWrapper
做同样的操作。
英文:
Summary
The problem is rooted in the design of your classes.
At the moment, your CustomerClient
class has hard dependencies on HttpPolicyBuilderAsync
and HttpClientWrapper
. It's an anti-pattern and bad practice to instantiate objects of other classes, especially external ones, inside of a constructor, unless they are simple structs or records without any functionality.
This is what inversion of control (IoC) containers are for. There is a plentitude of available IoC containers for C# that you could use to resolve your hard dependencies and remove the tight coupling from your classes, such as TinyIoC and SimpleInjector.
Since you cannot change the signature of the CustomerClient
constructor, as you mentioned in one of the comments, you can use an IoC container to pull in the dependencies. This, of course, means that your class will depend on the IoC container itself, which is a lot less bad than depending on an implementation inside of the constructor, because it removes tight coupling.
Solution
In order to solve your problem, you could use an IoC container to inject a provider into your class. In your production code, the provider implementation will provide a proper instance of HttpPolicyBuilderAsync
.
IHttpPolicyProvider interface
public interface IHttpPolicyProvider
{
IHttpPolicy GetPolicy(IConfigurationManager configManager, ILoggingService loggingService, string fileName, string svcName);
}
HttpPolicyBuilderProvider implementation
public class HttpPolicyBuilderProvider : IHttpPolicyProvider
{
public IHttpPolicy GetPolicy(IConfigurationManager configManager, ILoggingService loggingService, string fileName, string svcName)
{
return new HttpPolicyBuilderAsync(configManager, loggingService, fileName, svcName);
}
}
CustomerClient
Using TinyIoC, your class could be changed to look like this:
using TinyIoC;
public class CustomerClient : ICustomerClient
{
private readonly ILoggingService _loggingService;
private readonly ILoggingServiceConfigProvider _loggingServiceConfigProvider;
private readonly string _customerEndpoint;
private readonly IHttpPolicy _policy;
private readonly IHttpClientWrapper _httpClientWrapper;
private const string LOGGING_TITLE = "service";
private const string SERVICE_NAME = "service";
public CustomerClient(string customerEndpoint, ILoggingService loggingService, IConfigurationManager configManager,
ILoggingServiceConfigProvider loggingServiceConfigProvider)
{
_customerEndpoint = customerEndpoint ?? throw new ArgumentNullException(nameof(cpsEndpoint));
_loggingService = loggingService ?? throw new ArgumentNullException(nameof(loggingService));
_loggingServiceConfigProvider = loggingServiceConfigProvider ??
throw new ArgumentNullException(nameof(loggingServiceConfigProvider));
_customerEndpoint = customerEndpoint;
_policy = TinyIoCContainer.Current.Resolve<IHttpPolicyProvider>().GetPolicy(configManager, _loggingService, "customerPolicyOptions.json", LOGGING_TITLE);
_httpClientWrapper = new HttpClientWrapper(_cpsEndpoint, loggingService, SERVICE_NAME, _loggingServiceConfigProvider);
}
//...
}
Usage in Test
In your test code, you could then create a mock of the IHttpPolicyProvider
which returns a mock of IHttpPolicy
and register the provider with TinyIoC:
using TinyIoC;
[TestFixture]
public class CustomerClientTests
{
private Mock<ILoggingService> _loggingServiceMock;
private Mock<IConfigurationManager> _configManagerMock;
private Mock<ILoggingServiceConfigProvider> _loggingServiceConfigProviderMock;
private Mock<IHttpClientWrapper> _httpClientWrapperMock;
private CustomerClient _customerClient;
private Mock<IHttpPolicyProvider> _httpPolicyProviderMock;
private Mock<IHttpPolicy> _httpPolicyMock;
[SetUp]
public void Setup()
{
_loggingServiceMock = new Mock<ILoggingService>();
_configManagerMock = new Mock<IConfigurationManager>();
_loggingServiceConfigProviderMock = new Mock<ILoggingServiceConfigProvider>();
_httpClientWrapperMock = new Mock<IHttpClientWrapper>();
_httpPolicyProviderMock = new Mock<IHttpPolicyProvider>();
_httpPolicyMock = new Mock<IHttpPolicy>();
_httpPolicyProviderMock.Setup(provider => provider.GetPolicy(It.IsAny<IConfigurationManager>(), It.IsAny<ILoggingService>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(_httpPolicyMock.Object);
TinyIoCContainer.Current.Register<IHttpPolicyProvider>(_httpPolicyProviderMock.Object);
_customerClient = new CustomerClient("endpoint",_loggingServiceMock.Object, _configManagerMock.Object, _loggingServiceConfigProviderMock.Object);
}
//...
}
Usage in Production
In your production code, you would simply register the HttpPolicyBuilderProvider
using TinyIoC:
using TinyIoC;
public class YourApp()
{
public YourApp()
{
TinyIoCContainer.Current.Register<IHttpPolicyProvider, HttpPolicyBuilderProvider>();
}
}
NOTE: You'll need to do the same for the HttpClientWrapper
, as far as I can tell.
答案2
得分: 1
不要回答我要翻译的问题。以下是要翻译的内容:
如果你无法修改构造函数,你可以将代码隔离以获得 IHttpPolicy
,放入一个带有默认实现的 static internal
委托中。
public CustomerClient(string customerEndpoint, ILoggingService loggingService, IConfigurationManager configManager,
ILoggingServiceConfigProvider loggingServiceConfigProvider) {
// ...
_policy = PolicyGetter(configManager, loggingService);
// ...
}
static internal Func<IConfigurationManager, ILoggingService, IHttpPolicy> PolicyGetter =
(configManager, loggingService) => new HttpPolicyBuilderAsync(configManager, loggingService, "customerPolicyOptions.json", LOGGING_TITLE);
然后在你的测试类中,你可以更改 PolicyGetter
的值为任何你想要的值,在这种情况下,只需获取 IHttpPolicy
的模拟对象。
CustomerClient.PolicyGetter = (configManager, loggingService) => _httpPolicyMock.Object;
_customerClient = new CustomerClient("endpoint",_loggingServiceMock.Object, _configManagerMock.Object, _loggingServiceConfigProviderMock.Object);
将字段定义为 internal,并使你的程序集对测试项目 internal visible,以便在其他项目中无法更改实现。你也可以将其设置为 public,但这不是一个好主意。
英文:
If you can't modify the constructor you can isolate the code to get the IHttpPolicy
in a static internal
delegate with a default implementation.
public CustomerClient(string customerEndpoint, ILoggingService loggingService, IConfigurationManager configManager,
ILoggingServiceConfigProvider loggingServiceConfigProvider) {
// ...
_policy = PolicyGetter(configManager, loggingService);
// ...
}
static internal Func<IConfigurationManager, ILoggingService, IHttpPolicy> PolicyGetter =
(configManager, loggingService) => new HttpPolicyBuilderAsync(configManager, loggingService, "customerPolicyOptions.json", LOGGING_TITLE);
Then in your test class you could change the value for PolicyGetter
to anything you want, in this case simply get a mock object for IHttpPolicy
.
CustomerClient.PolicyGetter = (configManager, loggingService) => _httpPolicyMock.Object;
_customerClient = new CustomerClient("endpoint",_loggingServiceMock.Object, _configManagerMock.Object, _loggingServiceConfigProviderMock.Object);
Define the field as internal, and make your assembly internal visible to the test project, so in other project you cannot change the implementation. You could also make it public but it's not a good idea.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论