【问题标题】:Should you unit test constructors of abstract classes and, if so, how?您是否应该对抽象类的构造函数进行单元测试,如果是,如何?
【发布时间】:2023-03-16 23:40:01
【问题描述】:

假设你有一个像这样的抽象基类:

public abstract class WebApiServiceBase
    {
        public WebApiServiceBase(
            HttpClient httpClient)
        {
            HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        protected HttpClient HttpClient { get; }

// For brevity, omitted some instance methods that can be used by derived classes
}

该类知道它需要构造函数中的 HttpClient,因此如果它为 null,它会引发错误。是否应该对这种抛出错误的功能进行单元测试?

如果是这样,最好的方法是什么,因为您不能像这样在单元测试中直接实例化抽象类:

    [TestClass()]
    public class WebApiServiceBaseTests
    {
        [TestMethod()]
        public void WebApiServiceBase_NullHttpClient_ThrowsArgumentNull()
        {
            //Arrange
            Action action = () => {
                var controller = new WebApiServiceBase(null); // Won't compile, of course, because you can't directly instantiate an abstract class
            };

            //Act
            //Assert
            Assert.ThrowsException<ArgumentNullException>(action);
        }
    }

我知道我可以做很多事情:

  • 仅测试派生类中的错误抛出功能。
  • 在测试代码中创建一个新的派生类,仅用于测试。
  • 一开始就不要把这样的类抽象化。
  • 可能还有其他一些我没有想到的想法。

但是我应该做什么呢? 最佳实践是什么?

【问题讨论】:

  • 这里有一个正确答案的想法有点奇怪。
  • 嗯,编写单元测试的全部目的是获得代码覆盖率并建立一个系统,您可以轻松地验证您所做的更改是否不会破坏系统中的任何内容。由于您的类是抽象的并且永远不会被实例化,我认为尝试测试它没有意义。您确实在派生类中测试了这种功能,这隐含地测试了您的基类
  • 但是 Jochem,你不能也争辩说,因为没有派生类抛出 ArgumentNullException,你可能不会考虑在派生类中对它进行单元测试,所以你可能忘记测试了吗?

标签: c# unit-testing mstest


【解决方案1】:

这可行,但有点狡猾。除非您尝试访问模拟的某些属性,否则不会发生构造函数异常。它确实确认了某些东西正在引发您想要的异常。

[TestClass]
public class UnitTest1
{
    [TestMethod()]
    public void WebApiServiceBase_NullHttpClient_ThrowsArgumentNull()
    {

        var controller = new Mock<WebApiServiceBase>(MockBehavior.Loose, 
            //Pass null in to the constructor
            null
            );

        var exception = Assert.ThrowsException<TargetInvocationException>(() =>
        {
            var httpClient = controller.Object.HttpClient;
        });

        Assert.IsTrue(exception.InnerException is ArgumentNullException ane && ane.ParamName == "httpClient");
    }
}

但是,您不应该这样设计您的类,因为这会使它们难以测试。最好注入所有可重用的代码,而不是创建一个基类。一个好的 Http 客户端抽象意味着您不需要创建基类。这是一个很好的抽象:

public interface IClient
{
    /// <summary>
    /// Sends a strongly typed request to the server and waits for a strongly typed response
    /// </summary>
    /// <typeparam name="TResponseBody">The expected type of the response body</typeparam>
    /// <param name="request">The request that will be translated to a http request</param>
    /// <returns>The response as the strong type specified by TResponseBody /></returns>
    /// <typeparam name="TRequestBody"></typeparam>
    Task<Response<TResponseBody>> SendAsync<TResponseBody, TRequestBody>(IRequest<TRequestBody> request);

    /// <summary>
    /// Default headers to be sent with http requests
    /// </summary>
    IHeadersCollection DefaultRequestHeaders { get; }

    /// <summary>
    /// Base Uri for the client. Any resources specified on requests will be relative to this.
    /// </summary>
    AbsoluteUrl BaseUri { get; }
}

Reference

【讨论】:

    【解决方案2】:

    我可能会选择您的第二个选项(有一个派生类仅用于测试目的)。如果您使用域中的派生类,则可能稍后在抽象类仍被其他人使用时将其删除。然后测试将不再编译。因此,为这个显式测试用例创建一个类会更有意义。

    【讨论】:

    • 感谢您的推荐和合理的理由
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-19
    • 1970-01-01
    • 2021-02-25
    • 1970-01-01
    • 1970-01-01
    • 2021-09-04
    相关资源
    最近更新 更多