ASP.NETWebAPI單元測(cè)試-單元測(cè)試

今天來(lái)到了最后的壓軸章節(jié):?jiǎn)卧獪y(cè)試

十年的安溪網(wǎng)站建設(shè)經(jīng)驗(yàn),針對(duì)設(shè)計(jì)、前端、開(kāi)發(fā)、售后、文案、推廣等六對(duì)一服務(wù),響應(yīng)快,48小時(shí)及時(shí)工作處理。成都營(yíng)銷網(wǎng)站建設(shè)的優(yōu)勢(shì)是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動(dòng)調(diào)整安溪建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無(wú)論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。創(chuàng)新互聯(lián)建站從事“安溪網(wǎng)站設(shè)計(jì)”,“安溪網(wǎng)站推廣”以來(lái),每個(gè)客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。

我們已經(jīng)有了完整的程序結(jié)構(gòu),現(xiàn)在是時(shí)候來(lái)對(duì)我們的組件做單元測(cè)試了。

在UnitTestingWebAPI.Tests類庫(kù)上添加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 來(lái)寫單元測(cè)試

Services 單元測(cè)試


寫單元測(cè)試的第一件事是需要去設(shè)置或初始化一些單元測(cè)試中要用到的變量,NUnit框架則給要測(cè)試的方法添加Setup特性,在任何其他的NUnit測(cè)試開(kāi)始之前,這一方法會(huì)先執(zhí)行,把Services層注入到Controller的構(gòu)造函數(shù)之后的第一件事就是進(jìn)行單元測(cè)試。因此在對(duì)WebAPI進(jìn)行單元測(cè)試之前需要仿造Repositories和Service。

在這個(gè)例子中會(huì)看到如何仿造ArticleService, 并在這個(gè)Service的構(gòu)造函數(shù)中注入IArticleRepository和IUnitOfWork,所以我們需要?jiǎng)?chuàng)建兩個(gè)"特別的"實(shí)例來(lái)注入。

ArticleService Constructor

private readonly IArticleRepository articlesRepository;
private readonly IUnitOfWork unitOfWork;

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

這里的"特別的",是因?yàn)檫@些實(shí)例不是真正訪問(wèn)數(shù)據(jù)庫(kù)的實(shí)例.

注意

單元測(cè)試必須運(yùn)行在內(nèi)存中并且不應(yīng)該訪問(wèn)數(shù)據(jù)庫(kù). 所有核心的方法必須通過(guò)像我們的例子中用Mock這樣的框架仿造。這個(gè)方式自動(dòng)的測(cè)試會(huì)更快些。單元測(cè)試最基本的目的是更多的測(cè)試組件的行為,而不是真正的結(jié)果.

開(kāi)始測(cè)試ArticleService,創(chuàng)建一個(gè)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代碼可能會(huì)報(bào)錯(cuò):

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

ASP.NET Web API 單元測(cè)試 - 單元測(cè)試解決辦法:

在Assembiles中添加Microsoft.CSharp.dll

ASP.NET Web API 單元測(cè)試 - 單元測(cè)試

在SetupArticleRepository()方法中我們模仿了_articleRepository的行為,換句話說(shuō),當(dāng)一個(gè)特定的方法使用了這個(gè)Reporistory的實(shí)例,就會(huì)得到我們所期待的結(jié)果。然后我們?cè)赺articleService的構(gòu)造函數(shù)中注入這個(gè)實(shí)例。我們用下面代碼測(cè)試_articleService.GetArticles()的行為是否是我們所期待的.

ServiceShouldReturnAllArticles Test

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

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

編譯項(xiàng)目,運(yùn)行測(cè)試,要確保這個(gè)測(cè)試變?yōu)榫G色通過(guò)狀態(tài),用同樣的方式創(chuàng)建下面的測(cè)試:

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/p_w_picpaths/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 控制器單元測(cè)試

在熟悉了偽造Services行為測(cè)試的基礎(chǔ)上,來(lái)進(jìn)行WebAPI控制器的單元測(cè)試。

第一件事:設(shè)置在測(cè)試中需要的變量。

用下面的代碼創(chuàng)建用于測(cè)試的控制器:

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

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

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

    var result = _articlesController.GetArticles();

    CollectionAssert.AreEqual(result, _randomArticles);
}

請(qǐng)確保測(cè)試已綠色通過(guò),我們初始化了3條數(shù)據(jù),用_articlesController.GetArticle(3)測(cè)試看看能否返回最后一條。

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

測(cè)試一個(gè)無(wú)效的Update操作,必須失敗并且返回一個(gè)BadRequestResult, 重新調(diào)用設(shè)置在_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;
    }));

所以,當(dāng)我們測(cè)試一個(gè)不存在的文章就應(yīng)該返回失敗信息。

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

通過(guò)分別成功更新第一篇文章、發(fā)表一篇新文章、發(fā)布失敗一篇文章來(lái)完成我們的單元測(cè)試。

Controller 單元測(cè)試

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

上面測(cè)試的重點(diǎn),我們請(qǐng)求的幾個(gè)方面:返回碼或路由屬性。

管理 Handler單元測(cè)試

你可以通過(guò)創(chuàng)建HttpMessageInvoker的實(shí)例來(lái)測(cè)試Message Handler, 解析你要測(cè)試的Handler實(shí)例并調(diào)用SendAsync 方法。創(chuàng)建一個(gè)MessageHandlerTest.cs文件,并貼上下面的啟動(dòng)設(shè)置代碼

#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

我們?cè)贖eaderAppenderHandler的內(nèi)部設(shè)置另外一個(gè)可以終止請(qǐng)求的Hanlder.只要Uri中包含一個(gè)測(cè)試字符,從新調(diào)用EndRequestHandler將會(huì)終止請(qǐng)求.現(xiàn)在來(lái)測(cè)試.

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

假如要做一個(gè)集成測(cè)試:當(dāng)一個(gè)請(qǐng)求被消息管道分配到Controller的Action的真實(shí)behavior。

這將需要運(yùn)行WebApi,然后運(yùn)行單元測(cè)試。怎么做呢?必須是 通過(guò)Self host的模式運(yùn)行API,然后設(shè)置恰當(dāng)?shù)呐渲谩?/p>

在UnitTestingWebAPI.Tests的項(xiàng)目中添加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));
    }
}

媒體類型格式化器 測(cè)試

我們?cè)赨nitTestingWebAPI.API.Core中創(chuàng)建了ArticleFormatter,現(xiàn)在測(cè)試一下,應(yīng)該返回用逗號(hào)分割的文章字符串。它只能是寫文章的實(shí)例,但不能讀或者明白其它類型的類。為了應(yīng)用這個(gè)格式化器需要設(shè)置請(qǐng)求頭信息的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
}

我們可以創(chuàng)建一個(gè)ObjectContent來(lái)測(cè)試MediaTypeFormatter,傳遞一個(gè)對(duì)象來(lái)檢查是否能被被格式化,如果格式化器不能讀和寫傳遞過(guò)去的對(duì)象則會(huì)拋出異常,例如,文章的格式化器不能識(shí)別Blog對(duì)象:

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

換句話說(shuō),傳一個(gè)Article對(duì)象就一定會(huì)通過(guò)測(cè)試

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

用下面的代碼測(cè)試不符合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);
}

路由測(cè)試

在不Host Web API的情況下,測(cè)試路由配置。為了這個(gè)目的,需要一個(gè)可以從HttpControllerContext的實(shí)例中返回Controllerl類型或Controller中Action的幫助類,在測(cè)試之前,先創(chuàng)建一個(gè)路由配置的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
}

下面是路由測(cè)試:

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

測(cè)試一個(gè)請(qǐng)求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名稱,用同樣的方法來(lái)測(cè)試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()));
}

下面這個(gè)測(cè)試,路由會(huì)發(fā)生異常.

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

結(jié)論

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

嘗試在你的程序中總是寫單元測(cè)試,你不會(huì)后悔的。從里面會(huì)得到很多的好處,例如:在repository中一個(gè)簡(jiǎn)單的修改可能破壞很多方面,如果寫一個(gè)合適的測(cè)試,則可能破壞你程序的問(wèn)題會(huì)立即出現(xiàn).

原文:chsakell's Blog

網(wǎng)站標(biāo)題:ASP.NETWebAPI單元測(cè)試-單元測(cè)試
URL網(wǎng)址:http://bm7419.com/article4/gegioe.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供品牌網(wǎng)站建設(shè)、全網(wǎng)營(yíng)銷推廣網(wǎng)站導(dǎo)航、動(dòng)態(tài)網(wǎng)站網(wǎng)站制作、小程序開(kāi)發(fā)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

綿陽(yáng)服務(wù)器托管

網(wǎng)站設(shè)計(jì)公司知識(shí)