【问题标题】:how to test controller methods in c#如何在 C# 中测试控制器方法
【发布时间】:2018-09-06 00:33:48
【问题描述】:

我最近开始对 c# 代码进行单元测试,但我之前从未测试过控制器类。我有以下课程,我需要为此编写测试用例。

namespace nH.MasterData.API.Controllers
{
    [Produces("application/json")]
    [Route("api/AType")]
    public class ATypeController : Controller
    {
        private readonly IProcessor _domain;
        private readonly ILogger _logger;
        private const string Version = "VERSION_1";
        private const string Model = "model";
        private const string Entity = "AType";

        public ATypeController(IProxy proxy, ILogger<ATypeController> logger)
        {
            _logger = logger;
            var proxy = proxy.GetProxy();
            _domain = new InstitutionAddressProcessor(proxy);
        }

        [HttpGet]
        public OkObjectResult Get()
        {
            try
            {
                return Ok(_domain.GetATypesMembers(Model, Version, Entity, MemberType.Leaf, null));
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message + " " + ex.StackTrace);
                return new OkObjectResult(BadRequest());
            }
        }

    }
}

这里,_domain.GetATypesMembers() 不在我的项目中,我也没有显式创建_domain 的对象(它是在构造函数中创建的)。如何控制在构造函数中创建的对象?这样我就可以在调用时模拟响应而无需实际调用。

通常,我会将setup 模拟为wcfMockService.Setup(x =&gt; x.GetATypesMembers(...).ReturnAsync(response);,但是我该如何为这个对象编写呢?

感谢任何建议。

编辑1: 在构造函数中创建对象是不好的做法。但这是一个遗留代码,并在许多地方使用。所以我只是想在代码存在时编写测试。

【问题讨论】:

    标签: c# api controller nunit moq


    【解决方案1】:

    您亲身体验的是为什么将您的类与实现问题紧密耦合是糟糕的设计。

    该控制器应该被重构为依赖于抽象而不是具体化。

    namespace nH.MasterData.API.Controllers {
        [Produces("application/json")]
        [Route("api/AType")]
        public class ATypeController : Controller {
            private readonly IProcessor _domain;
            private readonly ILogger _logger;
            private const string Version = "VERSION_1";
            private const string Model = "model";
            private const string Entity = "AType";
    
            public ATypeController(IProcessor domain, ILogger<ATypeController> logger) {
                _logger = logger;            
                _domain = domain;
            }
    
            [HttpGet]
            public IActionResult Get() {
                try {
                    return Ok(_domain.GetATypesMembers(Model, Version, Entity, MemberType.Leaf, null));
                } catch (Exception ex) {
                    _logger.LogError(ex.Message + " " + ex.StackTrace);
                    return BadRequest();
                }
            }
        }
    }
    

    现在可以在测试时根据需要模拟依赖项

    var processor = new Mock<IProcessor>();
    var logger = new Mock<ILogger<ATypeController>>();
    var controller = new ATypeController(processor.Object, logger.Object); 
    
    //...setup mocks
    

    从原始构造函数的外观看来,IProcessor 应该进行相应配置以满足其依赖关系,因为它的构造函数对其依赖关系不真实,这需要在注入之前进行中间调用。

    这可以在组合根处得到满足。

    services.AddScoped<IProcessor>(_ => {                
        var proxy = _.GetService<IProxy>();
        return new InstitutionAddressProcessor(proxy.GetProxy());
    });
    

    【讨论】:

    • 是的,我知道这是最佳实践,但我无法修改该逻辑,因为它是遗留代码并在许多地方使用。你能建议我一种方法来测试这种方式吗?
    • @Extreme Moq 无法绕过控制器构造函数中的new InstitutionAddressProcessor
    • @Extreme 虽然可能更困难,但您能否模拟代理足以让 InstitutionAddressProcessor 实现被调用而不会产生任何不利影响?
    • @Nkosi,我不知道该怎么做。
    • @Extreme 显示InstitutionAddressProcessor 的相关代码,看看有没有。
    【解决方案2】:

    如果我正确理解您的情况,您将无法更正构造函数以使用正确的 DI 模型,并且您无法解决这个问题。假设你有一个有效的 IProxy 实例,你可以注入来正确地满足对象构造,你可以求助于一些肮脏的反射黑客来获取你的 IProcessor 模拟。

    类似的东西应该让你在构建你的 sut 之后设置字段。

    // Arrange
    var sut = SutProvider.GetATypeController(); // A system under test factory.
    
    var mock = new Mock<IProcessor>();
    // ... mock setup ..
    
    typeof(ATypeController)
        .GetField("_domain", BindingFlags.Instance | BindingFlags.NonPublic)
        .SetValue(sut, mock.Object);
    
    // Act
    var result = sut.Get();
    
    // Assert
    // ... assertions of result
    

    这不是一个理想的设置,但是在使用遗留代码时,您有时必须做一些肮脏的事情来设置对象的正确状态以进行测试。我强烈建议您仅将这些内容保留在测试中,并附上关于永远不要在“真实”代码中执行此操作的适当注释。

    SutProvider 是一个简单的工厂,实现可以是这样的:

    public class SutProvider 
    {
        public static ATypeController GetATypeController() => new ATypeController(GetProxy(), GetATypeControllerLogger());
    
        public static IProxy GetProxy() {
            // Either return a valid IProxy, or set up a mock that can return a result from the GetProxy method that is valid enough to withstand InstitutionAddressProcessor's constructor.
        }
    
        public static ILogger<ATypeController> GetATypeControllerLogger() => new Mock<ILogger<ATypeController>>().Object;
    }
    

    您可能需要深入了解InstitutionAddressProcessor 的构造函数,以找出将其传递给足够有效的代理的最佳方法。

    【讨论】:

    • 我会说这仍然需要删除只读标志,但这应该是一个小问题
    • @Simons0n readonly 在编译时使用,它不会阻止反射强制更改值,因此无需修改控制器即可继续。
    【解决方案3】:

    在这种情况下,我将使用Dependency Injection。它是Inversion Control 的一种形式。我不会在方法中传递代理和 _domain,而是将其作为参数传递(代理正在传递,但我会将 _domain 作为参数传递)。 _domain 将在Main like method 的某处实例化,或者可以使用['AutoFac']。然后很容易创建单元测试。

    这是使用Dependency Injection测试控制器的有用链接

    正如上述用户所指出的,存在紧密耦合,难以进行单元测试。

    【讨论】:

      【解决方案4】:

      您不会免费获得任何东西。如果您希望能够测试逻辑,则必须将事物解耦。如果您不将它们解耦,您将无法正确测试,因为您正在测试具有您自己无法测试的依赖关系的东西。所以没有 “单位”。

      话虽如此,它可能是遗留代码,但像 resharper 这样不错的工具(我完全不隶属于它)可以帮助您进行重构。注入服务后,您可以进行适当的单元测试。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-06-29
        • 2015-05-12
        • 2015-02-27
        • 2019-07-08
        • 2016-01-05
        • 2012-04-20
        • 2015-04-24
        • 1970-01-01
        相关资源
        最近更新 更多