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

Chapter 6. Things You Should Know

You have learned much, young one.

— Darth Vader The Empire Strikes Back (1980)

After having read the previous parts of this book, you should now be capable of writing unit tests. In fact, you should know a lot about things which some people would regard as "advanced" - for example parameterized tests and test doubles. Still, there are a great many other things you should also know, that will make your tasks easier to complete. You could probably figure them out yourself, using your existing knowledge and your general programming experience, but I would encourage you to continue your education here, and to read about custom patterns, frequently arising problems and various aspects of unit testing. There is still so much for you to learn!

In this section we will first discuss a few general aspects of writing tests (namely, advanced expected exceptions handling and the use of matchers), before proceeding with some how-to subsections devoted to such extremely common problems as testing collections, testing time-dependent methods, and the like.

6.1.What Values To Check?

[…]even for a small program, with a relatively simple set of variables and relatively few possible states per variable, the total number of possible valid states in combination is intractably large.

Michael Bolton DevelopSense

This section discusses a common problem related to the number of tests that should be written for a given function. It does not give an ultimate answer, but definitely sheds some light on this topic.

To test functionality you need to decide what arguments will be passed to methods. In general it is impossible to tests every combination of input arguments (for obvious reasons). This forces us to choose a subset of arguments, that will represent all possible cases. This might sound hard, but is usually quite simple. Let us discuss some tips for selecting the right arguments.

My general advice is as follows. You should select arguments that belong to these three groups: expected values (AKA happy path), boundary values, and strange values (AKA validity or domain).

6.1.1. Expected Values

This is how you expect a reasonable client1 will use your code. You expect that he will pass "John" as his name, "52" as his age and "yellow" as his favorite color. You expect him to pass values from 1 to 12 to the function that takes month as an input. You expect that he will pass a positive value to a constructor of Circle class that takes radius parameter. You expect that if he asks you to parse an XML file, to find all products with price greater than 30, then the file will at least be a valid XML. And so on…

This is something you absolutely need to check. Call your code with normal, reasonable, sensible, expected values, and make sure that it behaves properly, i.e. that it gives good answers. This is crucial. If your code does not work properly for such decent input arguments, it is useless.

1In this case client is someone or something that calls your code, not necessarily a person.

99

Chapter 6. Things You Should Know

6.1.2. Boundary Values

After you have checked that your code works fine for "normal" values, it is time to look for boundary values. Such values are a typical source of errors. You need to be especially inquisitive when checking for boundary values2.

In the previous section we chose arguments that represented expected values. It was always clear what your code should do when asked to work on them. In short, it should do its job. Now we choose arguments that are on the borderline. Sometimes we choose those that live inside a box labeled "reasonable arguments", sometimes we choose them from a box labeled "out of scope". When writing tests we need to decide which value belongs to which group3. This is when we decide whether, for example, -1 should result in an IllegalArgumentException or should cause a normal flow of calculations to ensue.

Let us discuss an example. Imagine a method that takes a single integer parameter and returns the number of days in a month. You assume that:

1 symbolizes January, 2 means February, …, 11 - November and 12 - December.

Now, let us test the boundary values. What we surely need to check is whether our counter really starts with 1 and not with 0. So we need to check that:

0 fails with an error, while 1 returns a value for January.

And similarly, we also need to check "the other end":

12 returns a value for December, while 13 fails with an error.

Analysis of the values that should be checked can lead you to some interesting findings. For example, it seems that the function that takes an integer representing a month could be improved by the introduction of enum Month. That way, passing of bad values will not be a problem anymore. This is an example of what happens when you start thinking about tests, and you consider boundary values. Redesign happens, and better design emerges. And that is a good thing!

6.1.3. Strange Values

So your code is almost bulletproof. It is verified to work well with expected values, and has some border checking capabilities. That is good, but not enough. The code is not yet ready to deal with values that are completely out of order.

No matter what values you expect, you can always meet with something unexpected. So, expect the unexpected, likely one of the following:

negative values, when only positive ones make sense (e.g. age),

null or empty values (e.g. empty Strings, empty collections),

data structures not conforming to some expectations, e.g. unsorted (when sorting is assumed) or with duplicates (when no duplicates are expected),

2See http://en.wikipedia.org/wiki/Boundary-value_analysis and http://en.wikipedia.org/wiki/Off-by-one_error for more information. 3This is true, when working in test-first manner, see Chapter 4, Test Driven Development.

100

Chapter 6. Things You Should Know

corrupted files (another mime-type than expected, not-valid XML, a binary file instead of text, etc.),

objects of a different type than expected (possible in weakly typed languages).

This kind of testing is especially important if your code deals with values entered by the user

– be it directly via some WWW form, or by reading information from a file uploaded from the user. When receiving data directly from users be prepared to expect every possible value!

Let us consider dates for example. Dates are a common source of confusion and error, because:

they are intrinsically complicated (e.g. leap years, different numbers of days in months),

we use them differently (i.e. various data formats – e.g. YYYY-MM-DD vs. YYYY-DD-MM).

So let us say that the function we are testing takes date as String. We expect it to be in YYYY-MM-DD format. The code is already tested to deal with:

normal values - 2011-01-18, 1956-07-14, 2015-12-23,

boundary values - 2011-02-28 and 2011-02-29, …

Now, it should be hardened against unexpected dates. For example, a date entered by a user might be one of the following:

2011-28-03 - which is YYYY-DD-MM, instead of the required YYYY-MM-DD,

2011/04/18 - which uses a different group separator,

tomorrow - which is a nice date, but not really what was expected,

null or empty String - this happens all the time,

http://some.address/, I love you Mom or blah (*&$ - it can also happen.

6.1.4. Should You Always Care?

"I will spend my life writing tests for this simple function…" If a dreadful thought like this crossed your mind while you were reading the previous section, maybe the next few paragraphs will cheer you up.

Let us say you write tests for AgeValidator class. Your web-based application will use it to check the validity of arguments passed from a web form (probably HTML). You know that this form will allow you to input only two digits in the age field. The question is, should you use this knowledge to limit the set of tested values to [0..99]? Does it make sense to write tests that pass 100 or -1 to validateAge() method?4

If you were to design all your classes and methods so that they did not depend on the context of your application, they would definitely be more robust and reusable, which in turn would certainly be a good thing. But should you strive for this? This is a common problem you will face. Let us discuss it.

4Let us assume that tampering with POST data is not possible…

101

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