ASP.NET Web API 單元測試 - 單元測試

今天來到了最後的壓軸章節:單元測試


我們已經有了完整的程序結構,現在是時候來對我們的組件做單元測試了。

在UnitTestingWebAPI.Tests類庫上添加UnitTestingWebAPI.Domain, UnitTestingWebAPI.Data, UnitTestingWebAPI.Service和UnitTestingWebAPI.API.Core 同樣要安裝下列的Nuget 包:


  1. Entity Framework

  2. Microsoft.AspNet.WebApi.Core

  3. Microsoft.AspNet.WebApi.Client

  4. Microsoft.AspNet.WebApi.Owin

  5. Microsoft.AspNet.WebApi.SelfHost

  6. Micoroft.Owin

  7. Owin

  8. Micoroft.Owin.Hosting

  9. Micoroft.Owin.Host.HttpListener

  10. Autofac.WebApi2

  11. NUnit

  12. NUnitTestAdapter


從清單中可知,我們將用NUnit 來寫單元測試


Services 單元測試


寫單元測試的第一件事是需要去設置或初始化一些單元測試中要用到的變量,NUnit框架則給要測試的方法添加Setup特性,在任何其他的NUnit測試開始之前,這一方法會先執行,把Services層注入到Controller的構造函數之後的第一件事就是進行單元測試。因此在對WebAPI進行單元測試之前需要仿造Repositories和Service。

在這個例子中會看到如何仿造ArticleService, 並在這個Service的構造函數中注入IArticleRepository和IUnitOfWork,所以我們需要創建兩個"特別的"實例來注入。


ArticleService Constructor

private readonly IArticleRepository articlesRepository;
private readonly IUnitOfWork unitOfWork;

public ArticleService(IArticleRepository articlesRepository, IUnitOfWork unitOfWork)
{
    this.articlesRepository = articlesRepository;
    this.unitOfWork = unitOfWork;
}


這裏的"特別的",是因爲這些實例不是真正訪問數據庫的實例.


注意


單元測試必須運行在內存中並且不應該訪問數據庫. 所有核心的方法必須通過像我們的例子中用Mock這樣的框架仿造。這個方式自動的測試會更快些。單元測試最基本的目的是更多的測試組件的行爲,而不是真正的結果.


開始測試ArticleService,創建一個ServiceTests的文件並添加下列代碼:

[TestFixture]
public class ServicesTests
{
    #region Variables
    IArticleService _articleService;
    IArticleRepository _articleRepository;
    IUnitOfWork _unitOfWork;
    List<Article> _randomArticles;
    #endregion
 
    #region Setup
    [SetUp]
    public void Setup()
    {
        _randomArticles = SetupArticles();
 
        _articleRepository = SetupArticleRepository();
        _unitOfWork = new Mock<IUnitOfWork>().Object;
        _articleService = new ArticleService(_articleRepository, _unitOfWork);
    }
 
    public List<Article> SetupArticles()
    {
        int _counter = new int();
        List<Article> _articles = BloggerInitializer.GetAllArticles();
 
        foreach (Article _article in _articles)
            _article.ID = ++_counter;
 
        return _articles;
    }

    public IArticleRepository SetupArticleRepository()
    {
        // Init repository
        var repo = new Mock<IArticleRepository>();

        // Setup mocking behavior
        repo.Setup(r => r.GetAll()).Returns(_randomArticles);

        repo.Setup(r => r.GetById(It.IsAny<int>()))
            .Returns(new Func<int, Article>(
                id => _randomArticles.Find(a => a.ID.Equals(id))));

        repo.Setup(r => r.Add(It.IsAny<Article>()))
            .Callback(new Action<Article>(newArticle =>
            {
                dynamic maxArticleID = _randomArticles.Last().ID;
                newArticle.ID = maxArticleID + 1;
                newArticle.DateCreated = DateTime.Now;
                _randomArticles.Add(newArticle);
            }));

        repo.Setup(r => r.Update(It.IsAny<Article>()))
            .Callback(new Action<Article>(x =>
            {
                var oldArticle = _randomArticles.Find(a => a.ID == x.ID);
                oldArticle.DateEdited = DateTime.Now;
                oldArticle = x;
            }));

        repo.Setup(r => r.Delete(It.IsAny<Article>()))
            .Callback(new Action<Article>(x =>
            {
                var _articleToRemove = _randomArticles.Find(a => a.ID == x.ID);

                if (_articleToRemove != null)
                    _randomArticles.Remove(_articleToRemove);
            }));

        // Return mock implementation
        return repo.Object;
    }
 
    #endregion
}

如果你直接copy代碼可能會報錯:

One or more types required to compile a dynaic expression ....

wKiom1haRLWivjg5AAAoRMGdg8s723.png解決辦法:

在Assembiles中添加Microsoft.CSharp.dll

wKioL1haRQywedYWAAAhnnShNFY680.png


在SetupArticleRepository()方法中我們模仿了_articleRepository的行爲,換句話說,當一個特定的方法使用了這個Reporistory的實例,就會得到我們所期待的結果。然後我們在_articleService的構造函數中注入這個實例。我們用下面代碼測試_articleService.GetArticles()的行爲是否是我們所期待的.

ServiceShouldReturnAllArticles Test

[Test]
public void ServiceShouldReturnAllArticles()
{
    var articles = _articleService.GetArticles();

    NUnit.Framework.Assert.That(articles, Is.EqualTo(_randomArticles));
}

編譯項目,運行測試,要確保這個測試變爲綠色通過狀態,用同樣的方式創建下面的測試:

Services Test

[Test]
public void ServiceShouldReturnRightArticle()
{
    var wcfSecurityArticle = _articleService.GetArticle(2);

    NUnit.Framework.Assert.That(wcfSecurityArticle,
        Is.EqualTo(_randomArticles.Find(a => a.Title.Contains("Secure WCF Services"))));
}

[Test]
public void ServiceShouldAddNewArticle()
{
    var _newArticle = new Article()
    {
        Author = "Chris Sakellarios",
        Contents = "If you are an ASP.NET MVC developer, you will certainly..",
        Title = "URL Rooting in ASP.NET (Web Forms)",
        URL = "https://chsakell.com/2013/12/15/url-rooting-in-asp-net-web-forms/"
    };

    int _maxArticleIDBeforeAdd = _randomArticles.Max(a => a.ID);
    _articleService.CreateArticle(_newArticle);

    NUnit.Framework.Assert.That(_newArticle, Is.EqualTo(_randomArticles.Last()));
    NUnit.Framework.Assert.That(_maxArticleIDBeforeAdd + 1, Is.EqualTo(_randomArticles.Last().ID));
}

[Test]
public void ServiceShouldUpdateArticle()
{
    var _firstArticle = _randomArticles.First();

    _firstArticle.Title = "OData feat. ASP.NET Web API"; // reversed<img draggable="false" class="emoji" alt="" src="https://s.w.org/images/core/emoji/2/svg/1f642.svg">
    _firstArticle.URL = "http://t.co/fuIbNoc7Zh"; // short link
    _articleService.UpdateArticle(_firstArticle);

    NUnit.Framework.Assert.That(_firstArticle.DateEdited, Is.Not.EqualTo(DateTime.MinValue));
    NUnit.Framework.Assert.That(_firstArticle.URL, Is.EqualTo("http://t.co/fuIbNoc7Zh"));
    NUnit.Framework.Assert.That(_firstArticle.ID, Is.EqualTo(1)); // hasn't changed
}

[Test]
public void ServiceShouldDeleteArticle()
{
    int maxID = _randomArticles.Max(a => a.ID); // Before removal
    var _lastArticle = _randomArticles.Last();

    // Remove last article
    _articleService.DeleteArticle(_lastArticle);

    NUnit.Framework.Assert.That(maxID, Is.GreaterThan(_randomArticles.Max(a => a.ID))); // Max reduced by 1
}

WebAPI 控制器單元測試

在熟悉了僞造Services行爲測試的基礎上,來進行WebAPI控制器的單元測試。

第一件事:設置在測試中需要的變量。

用下面的代碼創建用於測試的控制器:

    [TestFixture]
    public class ControllerTests
    {
        #region Variables
        IArticleService _articleService;
        IArticleRepository _articleRepository;
        IUnitOfWork _unitOfWork;
        List<Article> _randomArticles;
        #endregion

        #region Setup
        [SetUp]
        public void Setup()
        {
            _randomArticles = SetupArticles();

            _articleRepository = SetupArticleRepository();
            _unitOfWork = new Mock<IUnitOfWork>().Object;
            _articleService = new ArticleService(_articleRepository, _unitOfWork);
        }

        /// <summary>
        /// Setup Articles
        /// </summary>
        /// <returns></returns>
        public List<Article> SetupArticles()
        {
            int _counter = new int();
            List<Article> _articles = BloggerInitializer.GetAllArticles();

            foreach (Article _article in _articles)
                _article.ID = ++_counter;

            return _articles;
        }

        /// <summary>
        /// Emulate _articleRepository behavior
        /// </summary>
        /// <returns></returns>
        public IArticleRepository SetupArticleRepository()
        {
            // Init repository
            var repo = new Mock<IArticleRepository>();

            // Get all articles
            repo.Setup(r => r.GetAll()).Returns(_randomArticles);

            // Get Article by id
            repo.Setup(r => r.GetById(It.IsAny<int>()))
                .Returns(new Func<int, Article>(
                    id => _randomArticles.Find(a => a.ID.Equals(id))));

            // Add Article
            repo.Setup(r => r.Add(It.IsAny<Article>()))
                .Callback(new Action<Article>(newArticle =>
                {
                    dynamic maxArticleID = _randomArticles.Last().ID;
                    newArticle.ID = maxArticleID + 1;
                    newArticle.DateCreated = DateTime.Now;
                    _randomArticles.Add(newArticle);
                }));

            // Update Article
            repo.Setup(r => r.Update(It.IsAny<Article>()))
                .Callback(new Action<Article>(x =>
                {
                    var oldArticle = _randomArticles.Find(a => a.ID == x.ID);
                    oldArticle.DateEdited = DateTime.Now;
                    oldArticle.URL = x.URL;
                    oldArticle.Title = x.Title;
                    oldArticle.Contents = x.Contents;
                    oldArticle.BlogID = x.BlogID;
                }));

            // Delete Article
            repo.Setup(r => r.Delete(It.IsAny<Article>()))
                .Callback(new Action<Article>(x =>
                {
                    var _articleToRemove = _randomArticles.Find(a => a.ID == x.ID);

                    if (_articleToRemove != null)
                        _randomArticles.Remove(_articleToRemove);
                }));

            // Return mock implementation
            return repo.Object;
        }

        #endregion
    }


控制器的類和其它的類一樣,所以我們可以分開各自測試。下面測試_articlesController.GetArticles(),看看是否能返回所有的文章。

[Test]
public void ControlerShouldReturnAllArticles()
{
    var _articlesController = new ArticlesController(_articleService);

    var result = _articlesController.GetArticles();

    CollectionAssert.AreEqual(result, _randomArticles);
}

請確保測試已綠色通過,我們初始化了3條數據,用_articlesController.GetArticle(3)測試看看能否返回最後一條。

[Test]
public void ControlerShouldReturnLastArticle()
{
    var _articlesController = new ArticlesController(_articleService);

    var result = _articlesController.GetArticle(3) as OkNegotiatedContentResult<Article>;

    Assert.IsNotNull(result);
    Assert.AreEqual(result.Content.Title, _randomArticles.Last().Title);
}

測試一個無效的Update操作,必須失敗並且返回一個BadRequestResult, 重新調用設置在_articleRepository上的Update操作。

repo.Setup(r => r.Update(It.IsAny<Article>()))
    .Callback(new Action<Article>(x =>
    {
        var oldArticle = _randomArticles.Find(a => a.ID == x.ID);
        oldArticle.DateEdited = DateTime.Now;
        oldArticle.URL = x.URL;
        oldArticle.Title = x.Title;
        oldArticle.Contents = x.Contents;
        oldArticle.BlogID = x.BlogID;
    }));

所以,當我們測試一個不存在的文章就應該返回失敗信息。

[Test]
public void ControlerShouldPutReturnBadRequestResult()
{
    var _articlesController = new ArticlesController(_articleService)
    {
        Configuration = new HttpConfiguration(),
        Request = new HttpRequestMessage
        {
            Method = HttpMethod.Put,
            RequestUri = new Uri("http://localhost/api/articles/-1")
        }
    };

    var badresult = _articlesController.PutArticle(-1, new Article() { Title = "Unknown Article" });
    Assert.That(badresult, Is.TypeOf<BadRequestResult>());
}

通過分別成功更新第一篇文章、發表一篇新文章、發佈失敗一篇文章來完成我們的單元測試。


Controller 單元測試

[Test]
public void ControlerShouldPutUpdateFirstArticle()
{
    var _articlesController = new ArticlesController(_articleService)
    {
        Configuration = new HttpConfiguration(),
        Request = new HttpRequestMessage
        {
            Method = HttpMethod.Put,
            RequestUri = new Uri("http://localhost/api/articles/1")
        }
    };

    IHttpActionResult updateResult = _articlesController.PutArticle(1, new Article()
    {
        ID = 1,
        Title = "ASP.NET Web API feat. OData",
        URL = "http://t.co/fuIbNoc7Zh",
        Contents = @"OData is an open standard protocol.."
    }) as IHttpActionResult;

    Assert.That(updateResult, Is.TypeOf<StatusCodeResult>());

    StatusCodeResult statusCodeResult = updateResult as StatusCodeResult;

    Assert.That(statusCodeResult.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));

    Assert.That(_randomArticles.First().URL, Is.EqualTo("http://t.co/fuIbNoc7Zh"));
}

[Test]
public void ControlerShouldPostNewArticle()
{
    var article = new Article
    {
        Title = "Web API Unit Testing",
        URL = "https://chsakell.com/web-api-unit-testing",
        Author = "Chris Sakellarios",
        DateCreated = DateTime.Now,
        Contents = "Unit testing Web API.."
    };

    var _articlesController = new ArticlesController(_articleService)
    {
        Configuration = new HttpConfiguration(),
        Request = new HttpRequestMessage
        {
            Method = HttpMethod.Post,
            RequestUri = new Uri("http://localhost/api/articles")
        }
    };

    _articlesController.Configuration.MapHttpAttributeRoutes();
    _articlesController.Configuration.EnsureInitialized();
    _articlesController.RequestContext.RouteData = new HttpRouteData(
    new HttpRoute(), new HttpRouteValueDictionary { { "_articlesController", "Articles" } });
    var result = _articlesController.PostArticle(article) as CreatedAtRouteNegotiatedContentResult<Article>;

    Assert.That(result.RouteName, Is.EqualTo("DefaultApi"));
    Assert.That(result.Content.ID, Is.EqualTo(result.RouteValues["id"]));
    Assert.That(result.Content.ID, Is.EqualTo(_randomArticles.Max(a => a.ID)));
}

[Test]
public void ControlerShouldNotPostNewArticle()
{
    var article = new Article
    {
        Title = "Web API Unit Testing",
        URL = "https://chsakell.com/web-api-unit-testing",
        Author = "Chris Sakellarios",
        DateCreated = DateTime.Now,
        Contents = null
    };

    var _articlesController = new ArticlesController(_articleService)
    {
        Configuration = new HttpConfiguration(),
        Request = new HttpRequestMessage
        {
            Method = HttpMethod.Post,
            RequestUri = new Uri("http://localhost/api/articles")
        }
    };
    
    _articlesController.Configuration.MapHttpAttributeRoutes();
    _articlesController.Configuration.EnsureInitialized();
    _articlesController.RequestContext.RouteData = new HttpRouteData(
    new HttpRoute(), new HttpRouteValueDictionary { { "Controller", "Articles" } });
    _articlesController.ModelState.AddModelError("Contents", "Contents is required field");

    var result = _articlesController.PostArticle(article) as InvalidModelStateResult;

    Assert.That(result.ModelState.Count, Is.EqualTo(1));
    Assert.That(result.ModelState.IsValid, Is.EqualTo(false));
}

上面測試的重點,我們請求的幾個方面:返回碼或路由屬性。


管理 Handler單元測試

你可以通過創建HttpMessageInvoker的實例來測試Message Handler, 解析你要測試的Handler實例並調用SendAsync 方法。創建一個MessageHandlerTest.cs文件,並貼上下面的啓動設置代碼

#region Variables
private EndRequestHandler _endRequestHandler;
private HeaderAppenderHandler _headerAppenderHandler;
#endregion

#region Setup
[SetUp]
public void Setup()
{
    // Direct MessageHandler test
    _endRequestHandler = new EndRequestHandler();
    _headerAppenderHandler = new HeaderAppenderHandler()
    {
        InnerHandler = _endRequestHandler
    };
}
#endregion

我們在HeaderAppenderHandler的內部設置另外一個可以終止請求的Hanlder.只要Uri中包含一個測試字符,從新調用EndRequestHandler將會終止請求.現在來測試.

[Test]
public async void ShouldAppendCustomHeader()
{
    var invoker = new HttpMessageInvoker(_headerAppenderHandler);
    var result = await invoker.SendAsync(new HttpRequestMessage(HttpMethod.Get,
        new Uri("http://localhost/api/test/")), CancellationToken.None);

    Assert.That(result.Headers.Contains("X-WebAPI-Header"), Is.True);
    Assert.That(result.Content.ReadAsStringAsync().Result,
        Is.EqualTo("Unit testing message handlers!"));
}


假如要做一個集成測試:當一個請求被消息管道分配到Controller的Action的真實behavior。

這將需要運行WebApi,然後運行單元測試。怎麼做呢?必須是 通過Self host的模式運行API,然後設置恰當的配置。


在UnitTestingWebAPI.Tests的項目中添加Startup.cs文件:

Hosting/Startup.cs

public class Startup
{
    public void Configuration(IAppBuilder appBuilder)
    {
        var config = new HttpConfiguration();
        config.MessageHandlers.Add(new HeaderAppenderHandler());
        config.MessageHandlers.Add(new EndRequestHandler());
        config.Filters.Add(new ArticlesReversedFilter());
        config.Services.Replace(typeof(IAssembliesResolver), new CustomAssembliesResolver());

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
        config.MapHttpAttributeRoutes();

        // Autofac configuration
        var builder = new ContainerBuilder();
        builder.RegisterApiControllers(typeof(ArticlesController).Assembly);

        // Unit of Work
        var _unitOfWork = new Mock<IUnitOfWork>();
        builder.RegisterInstance(_unitOfWork.Object).As<IUnitOfWork>();

        //Repositories
        var _articlesRepository = new Mock<IArticleRepository>();
        _articlesRepository.Setup(x => x.GetAll()).Returns(
                BloggerInitializer.GetAllArticles()
            );
        builder.RegisterInstance(_articlesRepository.Object).As<IArticleRepository>();

        var _blogsRepository = new Mock<IBlogRepository>();
        _blogsRepository.Setup(x => x.GetAll()).Returns(
            BloggerInitializer.GetBlogs
            );
        builder.RegisterInstance(_blogsRepository.Object).As<IBlogRepository>();

        // Services
        builder.RegisterAssemblyTypes(typeof(ArticleService).Assembly)
            .Where(t => t.Name.EndsWith("Service"))
            .AsImplementedInterfaces().InstancePerRequest();

        builder.RegisterInstance(new ArticleService(_articlesRepository.Object, _unitOfWork.Object));
        builder.RegisterInstance(new BlogService(_blogsRepository.Object, _unitOfWork.Object));

        IContainer container = builder.Build();
        config.DependencyResolver = new AutofacWebApiDependencyResolver(container);

        appBuilder.UseWebApi(config);
    }
}

可能注意到和UnitTestingWebAPI.API裏的WebSetup類的不同之處在與,這裏我們用了假的Repositories和Services。

返回到ControllerTests.cs中。

[Test]
public void ShouldCallToControllerActionAppendCustomHeader()
{
    //Arrange
    var address = "http://localhost:9000/";

    using (WebApp.Start<Startup>(address))
    {
        HttpClient _client = new HttpClient();
        var response = _client.GetAsync(address + "api/articles").Result;

        Assert.That(response.Headers.Contains("X-WebAPI-Header"), Is.True);

        var _returnedArticles = response.Content.ReadAsAsync<List<Article>>().Result;
        Assert.That(_returnedArticles.Count, Is.EqualTo(BloggerInitializer.GetAllArticles().Count));
    }
}

媒體類型格式化器 測試

我們在UnitTestingWebAPI.API.Core中創建了ArticleFormatter,現在測試一下,應該返回用逗號分割的文章字符串。它只能是寫文章的實例,但不能讀或者明白其它類型的類。爲了應用這個格式化器需要設置請求頭信息的Accept爲application/article

[TestFixture]
public class MediaTypeFormatterTests
{
    #region Variables
    Blog _blog;
    Article _article;
    ArticleFormatter _formatter;
    #endregion

    #region Setup
    [SetUp]
    public void Setup()
    {
        _blog = BloggerInitializer.GetBlogs().First();
        _article = BloggerInitializer.GetChsakellsArticles().First();
        _formatter = new ArticleFormatter();
    }
    #endregion
}

我們可以創建一個ObjectContent來測試MediaTypeFormatter,傳遞一個對象來檢查是否能被被格式化,如果格式化器不能讀和寫傳遞過去的對象則會拋出異常,例如,文章的格式化器不能識別Blog對象:

[Test]
public void FormatterShouldThrowExceptionWhenUnsupportedType()
{
    Assert.Throws<InvalidOperationException>(() => new ObjectContent<Blog>(_blog, _formatter));
}

換句話說,傳一個Article對象就一定會通過測試

[Test]
public void FormatterShouldNotThrowExceptionWhenArticle()
{
    Assert.DoesNotThrow(() => new ObjectContent<Article>(_article, _formatter));
}

用下面的代碼測試不符合MediaType formatter的Media type

Media Type Formatters Unit tests

[Test]
public void FormatterShouldHeaderBeSetCorrectly()
{
    var content = new ObjectContent<Article>(_article, new ArticleFormatter());

    Assert.That(content.Headers.ContentType.MediaType, Is.EqualTo("application/article"));
}

[Test]
public async void FormatterShouldBeAbleToDeserializeArticle()
{
    var content = new ObjectContent<Article>(_article, _formatter);

    var deserializedItem = await content.ReadAsAsync<Article>(new[] { _formatter });

    Assert.That(_article, Is.SameAs(deserializedItem));
}

[Test]
public void FormatterShouldNotBeAbleToWriteUnsupportedType()
{
    var canWriteBlog = _formatter.CanWriteType(typeof(Blog));
    Assert.That(canWriteBlog, Is.False);
}

[Test]
public void FormatterShouldBeAbleToWriteArticle()
{
    var canWriteArticle = _formatter.CanWriteType(typeof(Article));
    Assert.That(canWriteArticle, Is.True);
}

路由測試

在不Host Web API的情況下,測試路由配置。爲了這個目的,需要一個可以從HttpControllerContext的實例中返回Controllerl類型或Controller中Action的幫助類,在測試之前,先創建一個路由配置的HttpConfiguration

Helpers/ControllerActionSelector.cs

public class ControllerActionSelector
{
    #region Variables
    HttpConfiguration config;
    HttpRequestMessage request;
    IHttpRouteData routeData;
    IHttpControllerSelector controllerSelector;
    HttpControllerContext controllerContext;
    #endregion

    #region Constructor
    public ControllerActionSelector(HttpConfiguration conf, HttpRequestMessage req)
    {
        config = conf;
        request = req;
        routeData = config.Routes.GetRouteData(request);
        request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;
        controllerSelector = new DefaultHttpControllerSelector(config);
        controllerContext = new HttpControllerContext(config, routeData, request);
    }
    #endregion

    #region Methods
    public string GetActionName()
    {
        if (controllerContext.ControllerDescriptor == null)
            GetControllerType();

        var actionSelector = new ApiControllerActionSelector();
        var descriptor = actionSelector.SelectAction(controllerContext);

        return descriptor.ActionName;
    }

    public Type GetControllerType()
    {
        var descriptor = controllerSelector.SelectController(request);
        controllerContext.ControllerDescriptor = descriptor;
        return descriptor.ControllerType;
    }
    #endregion
}

下面是路由測試:

[TestFixture]
public class RouteTests
{
    #region Variables
    HttpConfiguration _config;
    #endregion

    #region Setup
    [SetUp]
    public void Setup()
    {
        _config = new HttpConfiguration();
        _config.Routes.MapHttpRoute(name: "DefaultWebAPI", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional });
    }
    #endregion

    #region Helper methods
    public static string GetMethodName<T, U>(Expression<Func<T, U>> expression)
    {
        var method = expression.Body as MethodCallExpression;
        if (method != null)
            return method.Method.Name;

        throw new ArgumentException("Expression is wrong");
    }
    #endregion
}

測試一個請求api/articles/5到ArticleController的action GetArticle(int id)

[Test]
public void RouteShouldControllerGetArticleIsInvoked()
{
    var request = new HttpRequestMessage(HttpMethod.Get, "http://www.chsakell.com/api/articles/5");

    var _actionSelector = new ControllerActionSelector(_config, request);

    Assert.That(typeof(ArticlesController), Is.EqualTo(_actionSelector.GetControllerType()));
    Assert.That(GetMethodName((ArticlesController c) => c.GetArticle(5)),
        Is.EqualTo(_actionSelector.GetActionName()));
}

我們用反射得到controller的action名稱,用同樣的方法來測試post提交的action

[Test]
public void RouteShouldPostArticleActionIsInvoked()
{
    var request = new HttpRequestMessage(HttpMethod.Post, "http://www.chsakell.com/api/articles/");

    var _actionSelector = new ControllerActionSelector(_config, request);

    Assert.That(GetMethodName((ArticlesController c) =>
        c.PostArticle(new Article())), Is.EqualTo(_actionSelector.GetActionName()));
}

下面這個測試,路由會發生異常.

[Test]
public void RouteShouldInvalidRouteThrowException()
{
    var request = new HttpRequestMessage(HttpMethod.Post, "http://www.chsakell.com/api/InvalidController/");

    var _actionSelector = new ControllerActionSelector(_config, request);

    Assert.Throws<HttpResponseException>(() => _actionSelector.GetActionName());
}

結論

我們看到了Web API棧很多方面的單元測試,例如: mocking 服務層,單元測試控制器,消息管道,過濾器,定製媒體類型和路由配置。

嘗試在你的程序中總是寫單元測試,你不會後悔的。從裏面會得到很多的好處,例如:在repository中一個簡單的修改可能破壞很多方面,如果寫一個合適的測試,則可能破壞你程序的問題會立即出現.



原文chsakell's Blog

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章