我是否没有理解TDD单元测试(Asp.Net MVC项目)?

16
我正在尝试找出如何正确高效地对我的Asp.net MVC项目进行单元测试。当我开始这个项目时,我买了《Pro ASP.Net MVC》这本书,通过这本书,我学习了有关TDD和单元测试的知识。在看完这些示例之后,加上我现在公司中作为软件工程师在QA方面的工作,我惊讶地发现TDD是多么的棒。所以我开始为我的数据库层、业务层和控制器编写单元测试。在实现之前,所有内容都要经过单元测试。起初,我认为这很棒,但随后情况开始走下坡路。
以下是我开始遇到的问题:
- 我最终不得不编写应用程序代码,以使进行单元测试成为可能。我的意思不是代码有问题,必须修复才能通过单元测试。我的意思是,由于使用了Linq进行数据检索(使用泛型存储库模式),将数据库抽象化为模拟数据库是不可能的。
原因是,在Linq->sql或Linq->entities中,只需执行以下操作即可完成连接:
var objs = select p from _container.Projects select p.Objects;

然而,如果您对数据库层进行模拟,为了使该Linq通过单元测试,您必须更改Linq如下:

var objs = select p from _container.Projects
       join o in _container.Objects on o.ProjectId equals p.Id
       select o;
不仅这意味着你改变了应用逻辑,只是为了进行单元测试,而且为了可测试性而使代码效率降低,并且丧失了使用 ORM 的许多优势。此外,由于我很多模型的 ID 是数据库生成的,因此在处理非数据库测试时,我不得不编写额外的代码,因为 ID 从未生成,我仍然必须处理这些情况以使单元测试通过,但是它们永远不会在实际情况下发生。因此,最终我放弃了我的数据库单元测试。
对控制器编写单元测试很容易,只要返回视图。但是,我的应用程序的主要部分(也是最需要进行单元测试的部分)是一个复杂的 Ajax Web 应用程序。出于各种原因,我决定将应用程序从返回视图更改为返回所需数据的 JSON。 这之后,编写我的单元测试变得非常艰难,因为我没有找到任何好的方法来为非平凡的 JSON 编写单元测试。在苦苦挣扎和浪费大量时间尝试寻找好的方法来测试 JSON 后,我放弃了并删除了所有控制器单元测试(所有控制器操作都是针对该部分应用程序的)。
因此,最后我只能测试 Service 层(BLL)。目前我正在使用 EF4,但是我在使用 linq - > sql 时也遇到了这个问题。我选择了 EF4 模型先方法,因为对我来说,以这种方式定义业务对象并让框架想办法将其转换成 sql 后端是有意义的。开头还好,但由于关系变得繁琐起来。例如,假设我有 Project、User 和 Object 实体。一个 Object 必须与项目相关联,而项目必须与用户相关联。这不仅是数据库特定的规则,也是我的业务规则。但是,假设我想进行保存对象的单元测试(作为一个简单的示例),现在我必须执行以下代码才能确保保存成功:
User usr = new User { Name = "Me" };
_userService.SaveUser(usr);

Project prj = new Project { Name = "Test Project", Owner = usr };
_projectService.SaveProject(prj);

Object obj = new Object { Name = "Test Object" };
_objectService.SaveObject(obj);

// Perform verifications

进行单元测试需要付出很多努力,这会涉及到几个问题:

  • 首先,如果我添加一个新的依赖项,例如所有项目必须属于一个类别,那么我必须进入每个引用了该项目的单元测试中,添加保存类别的代码,然后添加将类别添加到项目中的代码。这对于一个非常简单的业务逻辑更改来说可能是一项巨大的工作,然而,我将修改的几乎所有单元测试实际上并不意味着要测试该功能/需求。
  • 如果我然后在我的SaveProject方法中添加验证,以便只能保存具有至少5个字符名称的项目,则我必须检查每个Object和Project单元测试,以确保新要求不会使任何不相关的单元测试失败。
  • 如果UserService.SaveUser()方法出现问题,它将导致所有项目和对象单元测试失败,并且除非进行异常排查,否则不会立即注意到原因。

因此,我已经从我的项目中删除了所有服务层单元测试。

我可以继续论述,但到目前为止,我还没有看到任何单元测试实际上可以帮助我而不会妨碍我的方式。我可以看到特定情况下我可以并可能会实现单元测试,例如确保我的数据验证方法正确工作,但这些情况很少见。我的一些问题可能可以缓解,但这不是没有添加额外层来进行单元测试就不能做到的,并因此使我的应用程序存在更多的失败点。

因此,在我的代码中已经没有单元测试了。幸运的是,我大量使用源代码控制,因此如果需要,我可以将它们恢复,但我只是看不到它们的意义。

在互联网上到处都是人们谈论TDD单元测试的好处,我不仅指那些狂热的人。很少有人对TDD/单元测试提出了无效的反驳,声称手动通过IDE调试更有效率,或者他们的编码技能非常棒,不需要单元测试。我认识到这两个观点都是完全错误的,尤其是对于需要由多个开发人员维护的项目,但是任何有效的反驳TDD似乎都很少见。

因此,本帖的重点是问,我是否没有理解如何使用TDD和自动单元测试?


你可能会更幸运,使用一本专门讲解ASP.NET MVC中的测试驱动开发的书籍,就像这本书一样。 (http://www.amazon.com/dp/0470447621) - Robert Harvey
我认为我已经找到了一种很好的方法,可以将EF4封装成一个可测试的存储库层,使用内存数据存储,这应该使得测试 MUCH 更简单地设置和执行。根据下面的答案,我也决定不测试JSON并不是一个坏主意(我的json非常复杂)。存储库代码基于http://elegantcode.com/2009/12/15/entity-framework-ef4-generic-repository-and-unit-of-work-prototype/上的代码。 - KallDrexx
3个回答

6
你可以查看我写的一个 ASP.NET MVC 2.0 示例项目结构。它展示了一些概念,可以帮助您开始对控制器逻辑和数据库进行单元测试。至于数据库测试,它不再是单元测试,而是集成测试。在我的示例中,我使用了NHibernate,它允许我轻松地切换到一个为每个测试夹具重新创建的SQLite示例数据库。
最后,在ASP.NET MVC中进行单元测试可能很痛苦,如果没有适当的关注点分离和抽象以及使用mocking框架和像MVCContrib.TestHelper这样的框架可以使您的生活更轻松。 这里是您的控制器单元测试的预览。
更新:
作为对评论的回应,我认为编程是一项具体的任务,很难给出一个终极答案,即如何对我的复杂业务应用程序进行单元测试。为了能够测试复杂应用程序,层之间的耦合应尽可能弱,这可以通过接口和抽象类实现。但我也同意,要在一个复杂的应用程序中实现这样的弱耦合并不是一件简单的任务。
我可以给你一个建议:如果整个TDD概念很难理解,并且你看不到它的好处,那么这很好。没有人能证明TDD在所有情况下都有利。只需尝试以每个类都承担单一职责的方式设计您的应用程序。如果您发现自己在同一类中执行输入验证、SQL数据访问和异常处理,则表示您正在错误地使用它。一旦实现了此分离,您将发现单元测试变得更加容易,甚至可能达到单元测试驱动开发的阶段 :-)
至于如何使用MvcContrib.TestHelper对JsonResult进行单元测试,这是一个具体的问题,我给出一个具体的答案:
public class MyModel
{
    public string MyProperty { get; set; }
}

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return Json(new MyModel { MyProperty = "value" });
    }
}

而这是测试:

[TestMethod]
public void HomeController_Index_Action_Should_Return_Json_Representation_Of_MyModel()
{
    // arrange
    var sut = new HomeController();

    // act
    var actual = sut.Index();

    // assert
    actual
        .AssertResultIs<JsonResult>()
        .Data
        .ShouldBe<MyModel>("")
        .MyProperty
        .ShouldBe("value");
}

我有一个单独用于单元测试的SQL数据库,我认为我可以使用EF4与sqllite数据库,所以这不是问题。问题在于我已经有关注点分离,包括EF处理数据库层,我的业务层和我的应用程序层(MVC)。唯一促进测试的方法就是将我的业务层分开,这样就形成了2个非常薄的层,使查询路径变为控制器->服务->DTO->EF,其中大多数操作对所有服务来说都没什么用,这使得维护变得很困难。 - KallDrexx
同时,测试助手似乎不能帮助测试JSON结果。我也看不出您提供的代码如何处理对象依赖和更复杂的业务逻辑(例如添加新的验证规则或对象依赖将导致所有现有测试失败)。我理解Todd的架构,但我发现在非平凡的情况下很难实现。 - KallDrexx
我想我开始明白我的困惑所在了。我猜我把仓库层和业务/服务层结合在一起,很难想出如何将它们分开而不重复两者之间的方法(即使让仓库层和业务层都有一个GetProjectByID()方法)。 - KallDrexx
@DarinDimitrov 应该先做哪一个?先写测试还是先写服务然后再测试它? - AminM

3
听起来问题不在于单元测试,而在于你的工具/平台。我来自Java背景,所以我不必重写我的查询只是为了满足单元测试。
测试Json很麻烦。如果你不想测试它,那就不要测试;)测试并不是100%覆盖率。它是测试真正需要测试的东西。你更有可能在一个复杂的if表达式中出现错误,而不是将东西添加到映射中,然后将其转换为json。
如果你测试映射被正确创建...那么json也很可能是正确的。当然,它不总是这样,但你运行它并且它能正常工作时就会知道。或者不行。就是这么简单。
不过,我可以就第三个问题提供真实的评论。你真的不想创建大量的对象图形。但有一些方法可以做到这一点。
创建一个名为ObjectMother的单例。有像ObjectMother.createProject()这样的方法。里面创建虚拟项目实例。这样,12个测试可以使用这个方法,然后你只需要在一个地方更改它。记住,测试代码需要重构!
此外,你还可以查看类似于dbUnit的东西。在Java世界中就是这样。其想法是在每次运行测试之前,将数据库置于已知状态。它会在测试的设置/拆卸中自动执行。这意味着你的测试代码可以立即开始测试。实际的虚拟测试数据在脚本或xml文件中。
希望这能帮到你。

经过重新思考,我认为之前我的测试做得不正确,这导致了错误的Linq链接。我以ID为基础进行链接(即prj.UserId = User.Id),而不是按照面向对象的方式进行链接(prj.User = User),因此Linq查询无法按照编写的方式工作,必须为单元测试重写。我认为如果解决了这个问题,我就可以非常简单地进行单元测试,并且这将大大减少我对TDD的困惑。 - KallDrexx

1

就测试您的AJAX化视图而言,我建议您尝试使用WatiN等测试框架。

通过这个框架,您可以执行以下操作(来自他们网站的示例)。

[Test]
public void SearchForWatiNOnGoogle()
{
 using (var browser = new IE("http://www.google.com"))
 {
  browser.TextField(Find.ByName("q")).TypeText("WatiN");
  browser.Button(Find.ByName("btnG")).Click();

  Assert.IsTrue(browser.ContainsText("WatiN"));
 }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接