Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Manning.The.Art.of.Unit.Testing.with.Examples.in.dot.NET.Jun.2009.pdf
Скачиваний:
18
Добавлен:
21.03.2016
Размер:
9.67 Mб
Скачать

198

CHAPTER 7

The pillars of good tests

Using static instances in tests—A developer sets static state that’s used in other tests.

Here are some solutions:

Not setting up state before each test—This is a mandatory practice when

writing unit tests. Use either a setup method or call specific helper methods at the beginning of the test to ensure the state is what you expect it to be.

Using shared state—In many cases, you don’t need to share state at all. Having separate instances of an object for each test is the safest way to go.

Using static instances in tests—You need to be careful how your tests manage static state. Be sure to clean up the static state using setup or teardown methods. Sometimes it’s effective to use direct helper method calls to clearly reset the static state from within the test. If you’re testing singletons, it’s worth adding public or internal setters so your tests can reset them to a clean object instance.

Anti-pattern: external-shared-state corruption

This anti-pattern is similar to the in-memory state corruption pattern, but it happens in integration-style testing:

Tests touch shared resources (either in memory or in external resources, such as databases, filesystems, and so on) without cleaning up or rolling back any changes they make to those resources.

Tests don’t set up the initial state they need before they start running, relying on the state to be there.

Now that we’ve looked at isolating tests, let’s manage our asserts to make sure we get the full story when a test fails.

7.2.5Avoiding multiple asserts

To understand the problem of multiple asserts, let’s take a look at the example in listing 7.14.

Listing 7.14 A test that contains multiple asserts

[Test]

public void CheckVariousSumResults()

{

Writing maintainable tests

199

Assert.AreEqual(3, Sum(1001, 1, 2)); Assert.AreEqual(3, Sum(1, 1001, 2)); Assert.AreEqual(3, Sum(1, 2, 1001));

}

There’s more than one test in this test method. The author of the test method tried to save some time by including three tests as three simple asserts. What’s the problem here? When asserts fail, they throw exceptions. (In NUnit’s case, they throw a special AssertException that’s caught by the NUnit test runner, which understands this exception as a signal that the current test method has failed.) Once an assert clause throws an exception, no other line executes in the test method. That means that, if the first assert in listing 7.14 failed, the other two assert clauses would never execute.

There are several ways to achieve the same goal:

Create a separate test for each assert.

Use parameterized tests.

Wrap the assert call with try-catch.

Why does it matter if some asserts aren’t executed?

If only one assert fails, you never know if the other asserts in that same test method would have failed or not. You may think you know, but it’s an assumption until you can prove it with a failing or passing assert. When people see only part of the picture, they tend to make a judgment call about the state of the system, which can turn out wrong. The more information you have about all the asserts that have failed or passed, the better equipped you are to understand where in the system a bug may lie, and where it doesn’t.

I’ve gone on wild goose chases hunting for bugs that weren’t there because only one assert out of several failed. Had I bothered to check whether the other asserts failed or passed, I might have realized that the bug was in a different location.

Sometimes people go and find bugs that they think are real, but when they “fix” them, the assert that previously failed passes and the other asserts in that test fail (or continue to fail). Sometimes you can’t see the

200

CHAPTER 7

The pillars of good tests

full problem, so fixing part of it can introduce new bugs into the system, which will only be discovered after you’ve uncovered each assert’s result.

That’s why it’s important that all the asserts have a chance to run, even if other asserts have failed before. In most cases, that means putting single asserts in tests.

Refactoring into multiple tests

Multiple asserts are really multiple tests without the benefit of test isolation; a failing test causes the other asserts (tests) to not execute. Instead, we can create separate test methods with meaningful names that represent each test case. Listing 7.15 shows an example of refactoring from the code from listing 7.14.

Listing 7.15 A refactored test class with three different tests

[Test]

public void Sum_1001AsFirstParam_Returns3()

{

Assert.AreEqual(3, Sum(1001, 1, 2));

}

[Test]

public void Sum_1001AsMiddleParam_Returns3()

{

Assert.AreEqual(3, Sum(1, 1001, 2));

}

[Test]

public void Sum_1001AsThirdParam_Returns3()

{

Assert.AreEqual(3, Sum(1, 2, 1001));

}

As you can see, the refactoring in listing 7.15 gives us three separate tests, each with a slightly different name indicating how we’re testing the unit. The benefit is that, if one of those tests fails, the others will still run. Unfortunately, this is too verbose, and most developers would feel this refactoring is overkill for the benefit. Although I disagree that

Writing maintainable tests

201

it’s overkill (it took about 20 seconds of work to get the benefit), I agree that the verbosity is an issue. It’s an issue because developers won’t do it, and we end up with our original problem.

That’s why many unit-testing frameworks, including MbUnit and NUnit, have a custom attribute you can use that achieves the same goal with much more concise syntax.

Using parameterized tests

Both MbUnit and NUnit support the notion of parameterized tests using a special attribute called [RowTest]. Listing 7.16 shows how you can use the [RowTest] and [Row] attributes (found in NUnit.Extensions.dll under the NUnit bin directory) to run the same test with different parameters in a single test method. Notice that, when you use the [RowTest] attribute, it replaces the [Test] attribute in NUnit.

Listing 7.16 A refactored test class using parameterized tests

[RowTest]

[Row(1001,1,2,3)]

[Row(1,1001,2,3)]

[Row(1,2,1001,3)]

public void SumTests(int x,int y, int z,int expected)

{

Assert.AreEqual(expected, Sum(x, y, z));

}

NOTE To use [RowTest] in NUnit, you’ll need to add a reference to NUnit.Extensions.dll, which is found in the bin directory of NUnit’s installation folder.

Parameterized test methods in NUnit and MbUnit are different from regular tests in that they can take parameters. They also expect at least one [RowTest] attribute to be placed on top of the current method instead of a regular [Test] attribute. The attribute takes any number of parameters, which are then mapped at runtime to the parameters that the test method expects in its signature.

The example in listing 7.16 expects four arguments. We call an assert method with the first three parameters, and use the last one as the

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]