Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test service unavailability and http error thrown

I am fairly new to Unit Testing and would like to simulate/test when a service is unavailable to assure that the correct error is thrown.

Scenario

REST API that queries Active Directory user accounts via LDAP/DirectorySearcher in C#. I see three possible outcomes: User Found, User Not Found, and Service Unavailable (DirectorySearcher). I have set up three tests for this, but one always fails depending if I am connected to the domain or not. When connected, Test #1, #2 succeed. When disconnected Test #2, #3 succeed. Is my testing overkill since the DirectoryServices library is already solid? My intention is to make sure that the web server throws an exception if it loses the ability to query 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);
                }
            }
        }
    }
}

Unit Tests

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);
        }
    }
}
like image 780
Mario Tacke Avatar asked Oct 01 '22 03:10

Mario Tacke


1 Answers

The best way to test something like this is using dependency injection for DirectorySearcher, and then using a mock in your unit test.

It looks like there is an IDirectorySearcher interface, although I don't know if DirectorySearcher implements it. In any event, and this might be more than your asking for, here's what I'd recommend:

  • Keep your controllers lightweight. Right now, you have a ton of non-reusable business logic in your action. You're trapping COM exceptions, and your controllers 'know' about low level AD workings. Instead, I'd write a wrapper to handle this, and throw a generic exception. You avoid a lot of duplicate code (the extra throw for both exceptions), and if you change how you work with AD, you can do it in a single place.

  • Inject the wrapper into your controller. This will let you mock the service, so you can test all the different paths through your action.

With your controller rewritten:

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

And then your unit test (in this case, I'm using moq as my mocking library):

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

The beauty of using a mock is that for each unit test, you can change the Setup() call to return whatever you want. It can return a SearchResult, or throw an exception, or do nothing at all. You can even use

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

to verify the call happened exactly 1 time (or none, or whatever).

This might be more than you asked for though, but in general, the less complex each layer, the easier each layer is to unit test.

like image 89
mfanto Avatar answered Oct 19 '22 00:10

mfanto