c# - 如何测试服务不可用和抛出的http错误

标签 c# visual-studio unit-testing

我是单元测试的新手,想在服务不可用时模拟/测试以确保抛出正确的错误。

场景

在 C# 中通过 LDAP/DirectorySearcher 查询 Active Directory 用户帐户的 REST API。我看到三种可能的结果:找到用户、未找到用户和服务不可用 (DirectorySearcher)。我为此设置了三个测试,但一个总是失败,这取决于我是否连接到域。连接后,测试#1、#2 成功。当断开测试 #2 时,#3 成功。既然 DirectoryServices 库已经很可靠,我的测试是否矫枉过正?我的目的是确保网络服务器在失去查询 Active Directory 的能力时抛出异常。

Controller

using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Web.Http;

namespace IdentitiesApi.Controllers
{
    public class UsersController : ApiController
    {
        // GET api/users/?username=admin
        public SearchResult Get([FromUri]string userName)
        {
            using (var searcher = new DirectorySearcher())
            {
                searcher.Filter = string.Format("(&(objectClass=user)(sAMAccountName={0}))", userName);

                try
                {
                    SearchResult user = searcher.FindOne();

                    if (user == null)
                    {
                        var response = new HttpResponseMessage(HttpStatusCode.NotFound)
                        {
                            Content = new StringContent(string.Format("No user with username = \"{0}\" found.", userName)),
                            ReasonPhrase = "User Not Found"
                        };

                        throw new HttpResponseException(response);
                    }
                    else
                    {
                        return user;
                    }

                }
                catch (COMException)
                {
                    var response = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
                    {
                        Content = new StringContent("The directory service could not be contacted. Please try again later."),
                        ReasonPhrase = "Directory Service Unavailable"
                    };

                    throw new HttpResponseException(response);
                }
            }
        }
    }
}

单元测试

using System;
using System.DirectoryServices;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Web.Http;
using IdentitiesApi.Controllers;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace IdentitiesApi.Test
{
    [TestClass]
    public class UsersTest
    {
        [TestMethod]
        public void Single_AD_User()
        {
            // arrange
            var controller = new UsersController();
            SearchResult searchResult;

            string userName = "admin"; // existing user name
            string expected = "admin";
            string actual = "";

            // act
            searchResult = controller.Get(userName);

            // assert
            foreach (object value in searchResult.Properties["samAccountName"])
            {
                actual = value.ToString();
            }

            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        [ExpectedException(typeof(HttpResponseException))]
        public void AD_User_Not_Found_Exception()
        {
            // arrange
            var controller = new UsersController();
            SearchResult searchResult;

            string userName = "?"; // invalid user name

            // act
            try
            {
                searchResult = controller.Get(userName);
            }
            catch (HttpResponseException ex)
            {
                // assert
                Assert.AreEqual(HttpStatusCode.NotFound, ex.Response.StatusCode);
                throw;
            }
        }

        [TestMethod]
        [ExpectedException(typeof(HttpResponseException))]
        public void AD_Service_Unavailable_Exception()
        {
            // arrange
            var controller = new UsersController();
            SearchResult searchResult;

            string userName = "admin";

            // act
            searchResult = controller.Get(userName);
        }
    }
}

最佳答案

测试此类内容的最佳方法是对 DirectorySearcher 使用依赖注入(inject),然后在单元测试中使用模拟。

看起来有一个 IDirectorySearcher接口(interface),虽然我不知道 DirectorySearcher 是否实现了它。无论如何,这可能超出您的要求,这是我的建议:

  • Keep your controllers lightweight .现在,您的操作中有大量不可重用的业务逻辑。您正在捕获 COM 异常,并且您的 Controller “知道”低级别的 AD 工作。相反,我会编写一个包装器来处理这个问题,并抛出一个通用异常。你避免了很多重复代码(两个异常的额外抛出),如果你改变你使用 AD 的方式,你可以在一个地方完成。

  • 将包装器注入(inject)您的 Controller 。这将使您可以模拟该服务,因此您可以通过您的操作测试所有不同的路径。

重写你的 Controller :

public class UsersController : ApiController
{
    private IDirectorySearcher _searcher;

    public UsersController(IDirectorySearcher searcher)
    {
        _searcher = searcher;
    }

    // GET api/users/?username=admin
    public SearchResult Get([FromUri]string userName)
    {
        try
        {
            return _searcher.FindSAMAccountName(userName);
        }

        catch (ADException ex)
        {
            var response = new HttpResponseMessage(HttpStatusCode.NotFound)
            {
                Content = ex.Content,
                ReasonPhrase = ex.Reason
            };

            throw new HttpResponseException(response);
        }
    }
}

然后是您的单元测试(在本例中,我使用 moq 作为我的模拟库):

    [TestMethod]
    [ExpectedException(typeof(HttpResponseException))]
    public void AD_User_Not_Found_Exception()
    {
        var searcher = new Mock<IDirectorySearcher>();

        searcher.Setup(x => x.FindSAMAccountName(It.IsAny<string>()).Throws(new ADException());

        var controller = new UsersController(searcher.Object);

        try
        {
            SearchResult searchResult = controller.Get("doesn't matter. any argument throws");
        }
        catch (HttpResponseException ex)
        {
            // assert
            Assert.AreEqual(HttpStatusCode.NotFound, ex.Response.StatusCode);
            throw;
        }
    }

使用 mock 的美妙之处在于,对于每个单元测试,您都可以更改 Setup() 调用以返回您想要的任何内容。它可以返回 SearchResult,或抛出异常,或什么都不做。你甚至可以使用

searcher.Verify(x => x.FindSAMAccountName(It.IsAny<string>()), Times.Once())

验证调用恰好发生了 1 次(或没有发生,或其他)。

虽然这可能比您要求的要多,但一般来说,每一层越不复杂,每一层就越容易进行单元测试。

关于c# - 如何测试服务不可用和抛出的http错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25193011/

相关文章:

c# - Fluent NHibernate 解决方案结构 - 如何在 Web 应用程序/控制台应用程序的后端引用一次 DLL

.net - ToString() 和调试器的字符串可视化工具

c# - 无法使用 IP 地址连接到应用程序

ruby-on-rails - 如何在 Rails 中使用 Test::Unit 测试记录器输出?

c# - 使用 Rhino Mocks 变干

c# - 单元测试 Asp.Net WebApi : how to test correct routing of a method with [FromUri] parameters

c# - X509 证书未在服务器上加载私钥文件

c# - 从数据库表生成类

c# - 从变量构造labell id

c++ - “cl”不被识别为内部或外部,vcvarsall