Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Tomek Kaczanowski - Practical Unit Testing with JUnit and Mockito - 2013.pdf
Скачиваний:
228
Добавлен:
07.03.2016
Размер:
6.59 Mб
Скачать

Chapter 3. Unit Tests with no Collaborators

Listing 3.6. Breaking the code so the test fails

public Money(int amount, String currency) { this.amount = 15;

this.currency = currency;

}

No matter what was passed by argument, amount will be set to 15.

Of course, this change will make one of the assertions in our test (assertEquals(10, money.getAmount());) fail. After rerunning the test, the following message and stacktrace will appear:

Listing 3.7. Failing test output

java.lang.AssertionError: Expected :10

Actual :15

at org.junit.Assert.fail(Assert.java:93)

at org.junit.Assert.failNotEquals(Assert.java:647) at org.junit.Assert.assertEquals(Assert.java:128) at org.junit.Assert.assertEquals(Assert.java:472) at org.junit.Assert.assertEquals(Assert.java:456) at com.practicalunittesting

.MoneyTest.constructorShouldSetAmountAndCurrency(MoneyTest.java:12)

This line informs us that an assertion has failed.

The values of both parameters of the assertEquals() assertion are printed: 10 was expected, but 15 was returned.

The first line of the stack trace which starts with something other than org.junit points to the offending line in your test code. In this case, it is the line which threw AssertionError:

assertEquals(10, money.getAmount());

One thing we should notice is that the order of assertions' parameters is really important. The printed information about the reason for the test failure is based on the assumption that we kept to the default order (remember: first expected value, then actual value). In any other circumstances, the printed information would have been misleading.

We will discuss the assertions' messages in details in Section 8.4.

3.6. Parameterized Tests

It is often advisable to test the same method with many different input values, expecting various outcomes5. In this section we will learn how JUnit can help us with this task.

3.6.1. The Problem

Let us assume, that apart from testing the Money class constructor with 10 USD, we also want to test it with 20 EUR. That can be done like this:

5We will discuss the problem of what values should be verified in Section 6.1.

24

Chapter 3. Unit Tests with no Collaborators

Listing 3.8. Testing the Money class with 10 USD and 20 EUR

public void constructorShouldSetAmountAndCurrency() { Money money = new Money(10, "USD");

assertEquals(10, money.getAmount()); assertEquals("USD", money.getCurrency());

money = new Money(20, "EUR");

assertEquals(20, money.getAmount()); assertEquals("EUR", money.getCurrency());

}

This approach will work, but its drawbacks are clearly visible. First of all, there is a lot of repetition and a clear violation of the DRY6 principle. Secondly, such code is usually created using the "copy&paste" technique, which is a sure recipe for getting into trouble by copying the whole section while only changing a part of it. Thirdly, the test class will grow with every new set of arguments. Enough! There must be a better way!

You can use various techniques to avoid repetitions such as those presented above. For example, you could introduce a for loop. That would make it better, but at the same time would result in the introduction of logic into your test (albeit of a very basic kind), which is not advisable (see Section 10.2). You could also divide the constructorShouldSetAmountAndCurrency() method into a number of smaller methods, each checking only one set of parameters. Yes, but that would have similar unwanted features to those of the naive approach discussed previously.

3.6.2. The Solution

Fortunately, you do not need to invent your own solution here. This requirement is so common, that testing frameworks offer some support for exactly these sorts of case. It is called "parameterized tests".

No matter which tool for parameterized tests you decide to use, the general idea is always the same. The test method, like the constructorShouldSetAmountAndCurrency() method discussed a moment ago, gets split into two parts. The first part acts as a source of arguments that will be passed to the second part (the actual test method), which, in turn, is responsible for the actual testing.

As mentioned earlier, JUnit’s support for parameterized tests is limited. We will use an additional, much more powerful library for this task: JUnitParams.

JUnitParams gives you choice when it comes to the implementation of the data-providing part. It can take the form of an annotation over the test method, or that of a separate test method. Please read the documentation for JUnitParams to learn about all its features.

Now let us have a look at the test code which uses the JUnitParams. There will be few new things, but we will discuss them one by one. The main thing to notice is that there is a clear separation of concerns:

the getMoney() method provides the data, and the constructorShouldSetAmountAndCurrency() method

provides the testing algorithm. The linking of these two methods is achieved thanks to the @Parameters annotation on the testing method.

6See http://en.wikipedia.org/wiki/Don’t_repeat_yourself.

25

Chapter 3. Unit Tests with no Collaborators

Listing 3.9. Parameterized test

@RunWith(JUnitParamsRunner.class) public class MoneyParameterizedTest {

private static final Object[] getMoney() { return new Object[] {

new Object[] {10, "USD"}, new Object[] {20, "EUR"}

};

}

@Test

@Parameters(method = "getMoney")

public void constructorShouldSetAmountAndCurrency( int amount, String currency) {

Money money = new Money(amount, currency);

assertEquals(amount, money.getAmount()); assertEquals(currency, money.getCurrency());

}

}

The @RunWith(JUnitParamsRunner.class) annotation on the test class informs JUnit that this test must be run with a special Runner provided by JUnitParams library. We do not need to get into details here: all we need to know is that we must have this annotation when using JUnitParams.

Data-providing methods are expected to return an Object[] array.

The constructorShouldSetAmountAndCurrency() method expects two parameters: amount and

currency. Each row of the array returned by the getMoney() method contains them both. The first set of arguments will consists of the number 10 and the currency USD.

The second set of arguments will consists of the number 20 and the currency EUR.

If a method uses a data-providing method, it will have to be annotated with a @Parameters annotation with the method attribute.

Instead of hardcoded values both arguments are used to create an object of the Money class and to verify it.

If you run the test on Listing 3.9, you will notice that JUnit has created two test cases "under the hood", and now it reports that two tests have been executed. What is more important, you will receive detailed information about what happened: i.e. what values were used for each test. With respect to MoneyTest, all you know is that the test passed. As regards MoneyParameterizedTest, you also know what parameters were involved. In the case of a successful test run this might not be a big deal, but in the case of a failure you will instantly know what combination of data caused it.

JUnitParams Dollar Sign Method

JUnitParams provides a useful $() (dollar sign) method which allows to write data-providing methods in a less verbose way.

Let us take this simple data-providing method as an example.

private static final Object[] getMoney() { return new Object[] {

new Object[] {10, "USD"}, new Object[] {20, "EUR"}

};

}

26

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