考虑以下单元测试示例。这些评论几乎解释了我的问题。
[TestMethod]
public void MyTestMethod()
{
//generate some objects in the database
...
//make an assert that fails sometimes (for example purposes, this fails always)
Assert.IsTrue(false);
//TODO: how do we clean up the data generated in the database now that the test has ended here?
}
答案 0 :(得分:26)
有两种方法可以做到这一点。 One正在测试类中的方法上使用TestInitialize和TestCleanup属性。它们将分别在测试之前和之后运行。
另一种方法是使用测试失败通过异常传播到测试运行器的事实。这意味着在断言失败后,可以使用测试中的try {} finally {}块来清理任何内容。
[TestMethod]
public void FooTest()
{
try
{
// setup some database objects
Foo foo = new Foo();
Bar bar = new Bar(foo);
Assert.Fail();
}
finally
{
// remove database objects.
}
}
如果要清理很多对象,那么try / finally清理会变得非常混乱。我的团队倾向于实现IDisposable的辅助类。它跟踪已创建的对象并将其推入堆栈。调用Dispose时,项目将从堆栈中弹出并从数据库中删除。
[TestMethod]
public void FooTest()
{
using (FooBarDatabaseContext context = new FooBarDatabaseContext())
{
// setup some db objects.
Foo foo = context.NewFoo();
Bar bar = context.NewBar(foo);
Assert.Fail();
} // calls dispose. deletes bar, then foo.
}
这具有在方法调用中包装构造函数的额外好处。如果构造函数签名发生变化,我们可以轻松修改测试代码。
答案 1 :(得分:9)
我认为在这种情况下最好的答案是仔细考虑你要测试的内容。理想情况下,单元测试应该尝试测试单个方法或函数的单个事实。当你开始将许多东西组合在一起时,它会进入集成测试的世界(这些测试同样有价值,但不同)。
对于单元测试目的,为了使您能够仅测试要测试的内容,您需要设计可测试性。这通常涉及额外使用接口(我假设您显示的代码中的.NET)和某种形式的依赖注入(但除非您需要,否则不需要IoC / DI容器)。它还受益于并鼓励您在系统中创建非常有凝聚力(单一用途)和解耦(软依赖性)类。
因此,当您测试依赖于数据库数据的业务逻辑时,通常会使用类似Repository Pattern的内容并注入fake/stub/mock IXXXRepository进行单元测试。在测试具体的存储库时,您需要进行所询问的数据库清理,或者需要对基础数据库调用进行填充/存根。这完全取决于你。
当您确实需要创建/填充/清理数据库时,您可能会考虑利用大多数测试框架中提供的各种设置和拆卸方法。但要小心,因为其中一些是在每次测试之前和之后运行,这会严重影响单元测试的性能。运行速度太慢的测试不会经常运行,这很糟糕。
在MS-Test中,用于声明设置/拆除的属性为ClassInitialize,ClassCleanUp,TestInitialize,TestCleanUp。其他框架也有类似命名的构造。
有许多框架可以帮助您进行模拟/存根:Moq,Rhino Mocks,NMock,TypeMock,Moles and Stubs(VS2010) ),VS11 Fakes(VS11 Beta)等。如果您正在寻找依赖注入框架,请查看Ninject,Unity,Castle Windsor等内容。
答案 2 :(得分:5)
一些回复:
如果它使用的是实际数据库,我认为这不是最严格意义上的“单元测试”。这是一个集成测试。单元测试应该没有这样的副作用。考虑使用模拟库来模拟实际的数据库。 Rhino Mocks是一个,但还有很多其他的。
但是,如果此测试的整个 point 实际上是与数据库进行交互,那么您将需要与瞬态测试数据库进行交互。在这种情况下,部分自动化测试将包括从头开始构建测试数据库的代码,然后运行测试,然后销毁测试数据库。同样,这个想法是没有外部副作用。可能有多种方法可以解决这个问题,而且我对单元测试框架不够熟悉,无法给出具体的建议。但是,如果您正在使用Visual Studio内置的测试,则可能会使用Visual Studio Database Project。
答案 3 :(得分:1)
你的问题有点过于笼统。通常你应该在每次测试后清理。通常,您不能依赖所有测试始终以相同的顺序执行,并且您必须确定数据库中的内容。对于一般设置或清理,大多数单元测试框架提供了可以覆盖的setUp和tearDown方法,并将自动调用。我不知道它在C#中是如何工作的,但是e。 G。在JUnit(Java)中,您有这些方法。
我同意大卫的观点。您的测试通常应该没有副作用。您应该为每个测试设置一个新数据库。
答案 4 :(得分:0)
在这种情况下你必须做手动清理。即,与db中生成一些对象相反。
另一种方法是使用诸如Rhino Mocks之类的Mocking工具,这样数据库就只是一个内存数据库
答案 5 :(得分:0)
这取决于您实际测试的内容。看看评论,我会说是,但顺便说一下,很难推断评论。在实践中,清理刚刚插入的对象,重置测试状态。因此,如果您进行清理,则开始从清理系统进行测试。
答案 6 :(得分:0)
我认为清理取决于你是如何构建数据的,所以如果“旧测试数据”不能与未来的测试运行相互作用,我认为把它留在后面是好的。
我在编写集成测试时一直采用的方法是让测试针对与应用程序db不同的数据库运行。我倾向于重建测试数据库作为每次测试运行的前提条件。这样,您就不需要为每个测试执行粒度清理方案,因为每次测试运行都会在运行之间获得干净的平台。 我使用SQL服务器完成了大部分开发工作,但在某些情况下,我在针对SQL Compact版本数据库运行测试时,可以快速有效地在运行之间重建。
答案 7 :(得分:0)
mbUnit有一个非常方便的属性Rollback,可以在完成测试后清理数据库。但是,您必须配置DTC(分布式事务处理协调器)才能使用它。
答案 8 :(得分:0)
我遇到了类似的问题,其中一个测试的断言阻止了清理并导致其他测试失败。
希望有时这对某人有用。
04/02/17
无论断言是通过还是失败,finally块都会执行,它也不会引入错误传递的风险 - 这会导致捕获!
答案 9 :(得分:0)
以下是我使用的测试方法的框架。这使我可以使用try catch finally来在finally块中执行清理代码,而不会丢失失败的断言。
[TestMethod]
public void TestMethod1()
{
Exception defaultException = new Exception("No real execption.");
try
{
#region Setup
#endregion
#region Tests
#endregion
}
catch (Exception exc)
{
/*if an Assert fails this catches its Exception so that it can be thrown
in the finally block*/
defaultException = exc;
}
finally
{
#region Cleanup
//cleanup code goes here
if (!defaultException.Message.Equals("No real execption."))
{
throw defaultException;
}
#endregion
}
}