你应该对 abstract 类 的构造函数进行单元测试吗?如果是,如何测试?

Should you unit test constructors of abstract classes and, if so, how?

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

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
}

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

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

    [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);
        }
    }

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

但是我应该做什么? 最佳做法是什么

我可能会选择你的第二个选项(有一个派生的 class 只是为了测试目的)。如果您使用来自您的域的派生 class,它可能会在以后被删除,而抽象 class 仍被其他人使用。然后测试将不再编译。因此,为这个显式测试用例设置 class 会更有意义。

这有效,但有点 dodgy.The 构造函数异常不会发生,直到您尝试访问模拟的某些 属性。它确实确认某些东西正在抛出您想要的异常。

[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");
    }
}

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

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