Have you ever had to care for a plant? You may not have a green thumb, but you probably know something about caring for a plant. You know that they need water, but not too much. You know that they need sunlight, and not to put it in your closet. What else? Soil? You’re unlikely to dump a plant with its roots onto your coffee table and expect it to survive. Depending on the plant, you remember and follow certain principles when you need to. These aren’t rules, but rather guidelines.
Software engineering has many great principles to help you avoid silly mistakes and write better code. Two great engineers, Tim Ottinger and Brett Schuchert, who wrote chapters of the book Clean Code, came up with an acronym which described principles they used to teach others how to write great tests: F.I.R.S.T.
Write great tests with the F.I.R.S.T. principles
F is for fast
How fast is fast? You should be aiming for many hundreds or thousands of tests per second. ⚡️Does that sound like a little too much? Not if each unit test only tests one class.
You'll know if your test isn’t fast when you start running it! According to Tim Ottinger, even a quarter of a second is painfully slow. Tests can build up - especially if you have thousands of them! I’ve known test suites which took hours, which makes it not feasible to run them regularly.
Let’s have a look at some real tests and what this looks like in the code:
As you saw, it's important to keep an eye on how long your tests take to run and investigate the outliers. Where you accidentally become dependent on slower dependencies, such as disks, try to abstract them away by using fakes in place of the real thing!
I is for isolated and independent
Have you ever needed to work through a complex problem by breaking it down into small pieces you could work on separately? Sometimes this makes it easier to stay focused and work your way through any issues. One issue = one cause of the problem = one solution. Tests are the same. When they fail, you want to understand why.
The arrange, act and assert principles we talked about earlier in the course, can help you here. Think about what happens if each test arranges its own class under test, and does only one assertion per test. This ensures that your tests are using separate data: you can isolate them from interfering with one another. There is very little overlap at that point, and so your tests remain independent.
As you can see, it’s easy for tests to fail due to no cause of their own when they are linked through a shared dependency. Unlike code, when you’re testing, it can be okay to repeat yourself if it preserves the isolation of your tests.
R is for repeatable
If you write a test which gives you confidence, it should tell you the same thing, no matter how often or where you run it. Sometimes tests become flaky and have to be run several times to make them pass.
Often when a test is not repeatable, it's because of a change made by one test impacting a later one. An example might be that one test writes a file to a local disk and that data gets queried by a second test, which then passes or fails depending on how smoothly the first write went. It might take a day of debugging the second test to realize the underlying problem was the first!
This doesn’t really fill you with much confidence. How can you be sure that this one pass is any more correct than the four previous failures? If you want to make your test repeatable, avoid any two tests leaking into each other. Remember that arrange from above? That’s where you make sure you start your tests with the new classes and data your tests depend on. Using those build tools discussed earlier in the course will ensure that you can build and test your software just as reliably on any environment - meaning your tests won’t fail for that reason!
In testing, these new classes, and the data that tests depend on are known collectively as test fixtures.
Let’s see this in action:
Did you catch the twist at the end? Our tests ran perfectly on Windows, but it was only through running them on another environment that we realized how easy it is to break the repeatability of our tests.
S is for self-validating
This was clearly thought up by a bunch of engineers. 🙂 Hands up if you think you know what self-validating means?
It validates...itself? 🤷🏽♂️
What it means is that running your test leaves it perfectly clear whether it passed or failed. JUnit does this and fails with red, which lets you red-green-refactor. Also, there is no such thing as an acceptable failure; something you hear from time to time. If you can’t rely on a test, it shouldn’t run. That’s what @Ignore
is for! 😉
By using a testing framework like JUnit, utilizing assertion libraries, and writing specific tests, you can ensure that if a test fails, there will be clear and unambiguous reporting that tells you exactly what passed or failed.
Let’s see this in action:
T is for thorough and timely
Now, according to Clean Code, T originally stood for timely. The idea was that your tests should be written as close to when you write your code as possible. By writing your test when you write your code, both the code and test can be designed to respect F.I.R.S.T.
In his book Clean Code, Tim Ottinger wrote:
“Testing post-facto requires developers to have the fortitude to refactor working code until they have a battery of tests that fulfill these FIRST principles.”
If you test first, you’re set up to thoroughly test your code, because it’s made for it. So let’s do that in the IDE and let a thorough and timely testing discipline help us build sound and well-tested code:
Today the T is often considered to stand for thorough, meaning that your code is tested extensively for positive and negative cases. Since the best way to write thorough tests is to make sure you’ve written thoroughly testable code, they point to the same outcome.
So...what should I be testing for?
To drive the design of your code through TDDing, and build that base of your testing pyramid, you want to ask some of the following questions:
Have I got a happy path test for each scenario I’ve coded?
Have I thought about negative paths for these scenarios? What if a vampire’s date of birth was after his death, due to resurrection? Could my system cope with this? Assuming you work in Transylvania? 🦇
Does each exception I throw get tested?
Is there a scenario where I don’t change the type of data I’m testing with, but can cause it to do something unexpected? What if I pass it a null or an empty string?
Did I remember security first? Sadly, it always comes as the last item on the list. Can this code only be run by the users who should run it? What if it’s not?
Asking such questions, and combining your tests with the other tiers of the pyramid can give you the confidence that you’ve been as thorough as possible.
Remember that principles are there to guide you. As you make choices in your software design, you can lean on them to remind you of what you should be considering. In the end, the choice is up to you.
Good tests keep on giving, and a bad test becomes something you can’t really trust. This defeats their purpose. If you follow the F.I.R.S.T. acronym, you’re all set to score yourself a strong foundation on the testing pyramid.
Try it out for yourself!
Now that you've seen examples of the F.I.R.S.T. principles, have a look at the tests and try running them yourself:
git clone https://github.com/OpenClassrooms-Student-Center/AchieveQualityThroughTestingInJava.git git checkout p2-c2-1-to-5
Or use your IDE to clone httphttps://github.com/OpenClassrooms-Student-Center/AchieveQualityThroughTestingInJava.gits://github.com/OpenClassrooms-Student-Center/AchieveQualityThroughTestingInJava.git and look at the branch p2-c2-1-to-5. Check out the README.md and see if you can add a test case for the division of two double
values.
You can now explore the examples from the screencast and try running the tests with:
mvn test
Naming your unit tests
Naming conventions go hand in hand with F.I.R.S.T. and are incredibly important for building readable code. How do you decide what you’re going to call your unit tests? The class name is easy; you typically group tests by the CUT, so the test class and class are named for one another. For example, MyClassTest would test MyClass. Easy, right? What about the method names?
For a long time, developers (particularly Java developers) had a horrible habit of following this same convention when writing unit tests. What does the method testAdd() tell you about the test? Well, you know it tests add, but is this a happy or sad path assertion? Am I testing negative numbers, positive numbers, or just checking that the method exists? More importantly, does it prove the test cases I’ve been working towards? I can’t tell by the name of the test, and so I definitely won’t be any wiser if it fails!
Developers have come up with a number of naming conventions for tests. Java typically uses camel case, which is a mixture of upperCaseAndLowerCaseLettersWithoutSpaces. However, when you’re writing tests, your goal is to communicate with people, and have test results you can talk about in the real world. Therefore, this rule usually falls by the wayside. Below are a few styles which exist to make tests clearer. For each method name construction style, you can see the important components which should go into creating your method names:
MethodName_StateUnderTest_ExpectedBehavior
Example: add_twoPositiveIntegers_returnsTheirSum()
Variant: Add_TwoPositiveIntegers_ReturnsTheirSum()
Note: This is not camel case, so the question of whether to start each new chunk with a capital is up to you.
MethodName_ExpectedBehavior_StateUnderTest
Example: add_returnsTheSum_ofTwoPositiveIntegers()
Variant: Add_ReturnsTheSum_OfTwoPositiveIntegers()
itShouldExpectedBehaviorConnectingWordStateUnderTest
Example: itShouldCalculateTheSumOfTwoPositiveIntegers()
Variant: testItCalcultesTheSumof..
givenStateUnderTest_whenMethodAction_thenExpectedBehavior
Example: givenTwoPostiveIntegers_whenAdded_thenTheyShouldBeSummed
Variant: givenTwoPositiveIntegerWhenAddedThenTheyShouldBeSummed()
You’re free to mix it up as long as you are consistent, and have proved that your reason for change gives everyone who reads your tests greater clarity.
Remember, these are just a few of many styles you’ll see out there! While the styles in rows three and four read more naturally, in my opinion, the first two are great when you’re building a strong test base and want to be sure that you’ve thoroughly tested each method. This is because the first argument is the method name, and the second is forcing you to consider all the different types of input you may encounter. The example above, for instance, focuses on two positive integers. What about having one negative, or one null? Writing your tests can then help drive the situations your code for.
The underlying goal across all the naming styles is to ensure you clearly communicate what you’re testing. You’ve already seen that JUnit5 lets you use the @DisplayName
attribute to better name your tests. This complements a good method name, and allows you to make sure you clearly communicate what you’re testing. Whatever you pick, or find in a project, remember to be consistent.
Let's recap!
Make sure your tests have names that clearly describe what they are testing. Pick a style and stick to it!
Use the F.I.R.S.T. principles to keep your tests:
PRINCIPLE | What does this mean? |
Fast | Even half a second is too slow. You need thousands of tests to run in less than a few seconds. |
Isolated and independent | When a test fails, you want to know what has failed. Test one thing at a time and only use more than one assertion if you must. |
Repeatable | You can’t trust tests which sometimes fail or sometimes pass. You need to build tests which avoid interfering with one another and are self-sufficient enough to always give the same result. |
Self-validating | Use the available assertion libraries, write specific tests, and let your testing framework take care of everything else. You can trust it to run and present clear reports on your tests. |
Timely and thorough | Use TDD and write your tests as you write your code. Explore the full breadth of things your method could do and write lots of unit tests. |