This article by the Code Rural Network – Sun Tenghao original translation, reproduced please see the text at the end of the reprint requirements, welcome to participate in our paid contribution program!
On one occasion, I heard in a lecture that the host asked the audience how to deliberately write code that was difficult to test. The presence of the small partners were stunned, because no one would intentionally write this bad code. I remember that they couldn't even give a good answer.
Of course, the aim of this question is not to teach you how to write a bad code that makes a colleague cry without tears. But to understand what kind of code is difficult to test, to avoid these serious problems.
Here's my answer to the question above (which is, of course, my personal point of view, and everyone hates it differently.) )
1. Use a large number of static fields
In particular, shared static collection classes in different classes, such as the following:
PublicClassCatalog {Privatestatic list<person> people = new arraylist<> (); public void Addperson (person person) { if (calendar.getinstance (). Get (Calendar.year)-Person.getyearofbirth () < ) { throw new IllegalArgumentException ("only Adults admitted. "); People.add (person); } public int getnrofpeople () { return people.size ();}}
Now let's take a look at the test code:
PublicClasscatalogtest {PrivateFinal Catalog undertest =New Catalog ();@Test (Expected=illegalargumentexception.class)Publicvoid addingaminorshouldthrowexception () {assertequals (0, Undertest.getnrofpeople ()); //precondition person p = new person (2015); Undertest.addperson (P); } @Test public void addinganadultshouldsucceed () {assertequals (0, Undertest.getnrofpeople ()); //precondition, or is it? Person p = new person (1985); Undertest.addperson (P); Assertequals (1, Undertest.getnrofpeople ())}}
If you run this two test, you will find that the use case that expects to throw an exception fails. This may make you suspect life, but JUnit is free to arrange the order of use case execution without relying on the order in which the use cases are written. In this code, the second Test case runs first, it detects that the collection is empty, and then successfully registers a adult. Since our collection is static, the first test case detects that the collection is not empty (we have already put a people in the previous test case), so we failed.
Once we remove the static keyword, two test cases are executed successfully. Before each test case executes, JUnit initializes the fields in the test case (not just the fields in the @before annotation method). Now we have an instance member instead of a static people list bound on the class. This means that each test case will be created with a new catalog object, including a new list, before it runs. Every time we have a new list of empty people.
Of course, in this case we can easily find and solve this problem, but imagine a large system, there are many kinds of people list of operations.
A static mutable set (as my colleague says) is like a trash can, filled with rubbish, and should really be avoided.
2. Declaring numerous final classes
A class declaration final prevents the mock. Here's what happens when you try to mock a final class with Mockito:
Org.mockito.exceptions.base. following: -final Classes -Anonymous classes -primitive types at Org.mockito.internal.runners.JUnit45AndHigherRunnerImpl ...
A class declares that final has serious consequences (such as it cannot be inherited and mocks), so there is good reason not to do so.
Wait, don't give up treatment. In the face of a final class, only a few other efforts are needed. Powermock can mock such classes, but I think this kind of library is not a cure for the symptoms.
Another way is to create a non-fianl package class, wrap the final class and Mock the package class. Of course, the premise is that you can avoid changes in the possible class signatures.
3. Always create objects in methods, especially in constructors.
I don't think that's too much to explain. Creating an object in a method or constructor makes it impossible to introduce a copy of the test. By completely hard-coding the dependencies, we can't mock, write real unit tests (excluding all external dependencies, and quickly test a class in a standalone environment).
Dependencies should be injected, primarily through constructors. If for some reason this is not possible, the work of creating an object should be placed in a protected method, so that the fictitious class inheriting it can override the method.
4. The name and content of the method/test are always inconsistent
Many people think that the long method (long methods) is the number one public enemy of testing. Although this is true in many cases, it is not absolute. Imagine a small piece of code that looks for a long square root function. It takes an integer and returns a floating-point number. Because we know how to ask for the square root, we don't need to care about the details of the code implementation. We used this method as a black box to measure some of the obvious values (9,25,36) and some uncommon values.
However, if this method is called log (the log in mathematics), then the test we are going to write is farfetched. This is a waste of time, and the test cases written are completely useless.
The test method is the same. Once the test fails, it is really useful to describe the test name of the test work. For example, the test name is throwsexceptionfornegativenumbers, a positive number is tested and there are no exceptions, which is helpful for us to understand what we are testing.
5. The flow operation is never divided into several instructions
Because Java 8 's streams has a fluent interface, this does not mean that filter,map,flatmap and other operations are followed by a chain call (or nested invocation).
Let's look at an example. Each football club provides a list of soccer players (football players), returning 25-year-old to 30-year-old striker players.
Publicmap<String,Integer> Somemethodname (list<player> players) {return Players.stream (). filter (player, {int now = calendar.getinstance (). get (calendar. year); return (Now-player.getyearofbirth () < 30) && (now- Player.getyearofbirth () > 25); }). filter (player, player.getposition () = = position.st). Collect (collectors.groupingby (Player:: Getplaysfor, collectors.summinglong (player::getvalue)));}
The problem here is that it is difficult to know what kind of data should be passed through such a chained method, so traverse all possible data routes. It's hard to point out what kind of players (athlete) list we want to use as input.
A long list of operations is attractive, but it is very readable. In general, it's a good idea to split them into blocks of code based on neat code rules, and extract them into variables or methods.
After some extraction, the code is refactored as follows
Publicmap<String,Integer> Somemethodname (list<Player> players) {stream<player> Playersinagerange = Players.stream ().Filter (Isplayerinagerange ());stream<Player> strikers = Playersinagerange.Filter (Isplayerstriker ());Return Strikers.collect (Collectors.groupingby (Player::getplaysfor,Collectors.summinglong (Player::getvalue)));}private PREDICATE<? super player> Isplayerstriker () { return player, player.getposition () = = position.ST;} private PREDICATE<? super player> Isplayerinagerange () { return player, {int now = calendar.getinstance (). get (calendar. year); return (Now-player.getyearofbirth () < 30) && (now- Player.getyearofbirth () > 25); };}
Although the code is somewhat long, readability is greatly improved.
Link: http://www.codeceo.com/article/5-ways-write-hard-to-test-code.html
English Original: 5 easy Ways to Write hard-to-test Code
Translation Code Rural Network – Sun Tenghao
[ reproduced in the text must be marked and retained in the original link, translation links and translators and other information. ]
5 ways to write code that is difficult to test