How To Clean Up After All Junit Tests
Today I saw notwithstanding some other JUnit examination that was extending a superclass…this is me but after:
If you lot're writing JUnit tests on a daily ground, you probably experienced that moment when you realize that you're duplicating some code (possibly some set up code or a group of assertions) across multiple tests. Yous might recall that, in order to avoid code duplication, y'all can create a superclass to share all the common code.
That's probably not going to be a corking idea…
In this blog postal service, we'll see how to create a JUnit Rule
in Kotlin, and how to utilize annotations to make them easy to configure and have more than elegant tests.
Why inheritance in your tests is a bad thought
Your tests are 1 of the primary sources of documentation of your codebase. You want them to be articulate and cocky-explanatory. Ideally, y'all should exist able to print them on a paper a reader should be able to understand them.
Having a test that inherits from a superclass is going against this. You strength a reader to open another class to understand what's the real behavior of a exam. This hidden logic can be actually annoying and can pb to a lot of headaches when debugging a test.
class Test : AbstractTest () { @Test fun testSomething () { assertEquals ( "what can" , "get wrong" ) } }
For instance, this test might expect obviously failing only could be not. Inside the AbstractTest
form, the developer could have re-defined the assertEquals(String, String)
to check if the two parameters accept the same length rather than the same content.
Moreover, you're adding a superclass with the intention of sharing code, but the problem is that you're not making it reusable.
Let's say that you have your grouping of tests, ItLocaleTest.kt
, DeLocaleTest.kt
, with a superclass (say AbstractLocaleTest.kt
) that has the lawmaking to set up upward the locale for your testing surround. So you have another group of tests say LoggedUserTest.kt
, AnoymousUserTest.kt
with another superclass (say AbstractUserTest.kt
) that has the lawmaking to prepare the authentication token for your user.
What if tomorrow you want to write a test that has both initializations, like a logged in user with the FR locale? Unfortunately, you tin't because Kotlin/Coffee don't support multiple inheritance (your test tin have merely one superclass).
And then in this example y'all probably have to use composition over inheritance. You lot need to extract your initialization logic in some helper course that will be used by all the tests that needs information technology. In this way y'all can compose multiple helper classes and combine all the initializations you need and y'all're non stuck with only one unmarried superclass.
The JUnit framework offers us some tools to provide initialization lawmaking:
- An Notation to run a method before all the test in a file (
@BeforeClass
in JUnit4 or@BeforeAll
in JUnit5) - An Annotation to run a method before every test in a file (
@Before
in JUnit4 or@BeforeEach
in JUnit5) - An Annotation to run a method later all the test in a file (
@AfterClass
in JUnit4 or@AfterAll
in JUnit5) - An Note to run a method after every test in a file (
@Afterward
in JUnit4 or@AfterEach
in JUnit5)
Just the all-time tool to reuse code are Rules. JUnit Rules are a simple mode to alter the behavior of all the tests in a class.
The JUnit defines them in this fashion:
Rules can do everything that could be done previously with methods annotated with @Earlier, @After, @BeforeClass, or @AfterClass, just they are more than powerful, and more easily shared betwixt projects and classes.
Multiple rules tin can exist combined together with a RuleChain assuasive us to create initialization code that can be hands combined, reused, and distributed across projects and teams.
A simple JUnit @Rule
Please annotation that those examples apply only for JUnit4 equally JUnit5 requires a minSdkVersion
of 26
or to a higher place for Instrumentation tests on Android (which is not the case for several apps). Rules have been replaced by the Extension API in JUnit5.
To create a Rule, you need to implement the TestRule
interface. This interface has just one method: apply
. With this method, you can specify how your Rule should alter the test execution.
You tin meet the apply
conceptually as a map()
. It takes a Argument
as input and returns another Argument
as output.
Let's run across an case of a Rule that volition practise nothing:
class EmptyRule : TestRule { override fun apply ( due south : Argument , d : Description ): Statement { render s } }
While something more interesting might exist a Dominion that prints the execution time of every test:
grade LogTimingRule : TestRule { override fun apply ( southward : Statement , d : Description ): Statement { return object : Argument () { override fun evaluate () { // Practise something before the test. val startTime = Arrangement . currentTimeMillis () try { // Execute the test. southward . evaluate () } finally { // Do something after the test. val endTime = System . currentTimeMillis () println ( "${d.methodName} took ${endTime - startTime} ms)" ) } } } } }
Here you lot can run across that we alter the exam to render a new Argument
that will tape the offset time, execute the old statement, and impress the elapsed time on the panel.
Using this Rule will be merely one line of code:
@ become : Dominion val timingRule = LogTimingRule ()
The @Dominion
annotation will make sure your Rule is executed Before every test.
Annotations + @Rule = <three
Smashing! Then now nosotros know how to write Rules.
What nigh if you desire to customize your dominion for every single exam? For example, I might want to turn on the logging of the timing only for some specific test.
You lot might have noticed that at that place is one small detail that I left behind: the apply
method has ii parameters.
The second parameter is a Description
. This parameter gives usa access to some metadata for every examination. A way to customize our Rule to be more flexible for every test is to use annotations, and the Description
class has exactly all the methods to give us this support.
Permit's change the LogTimingRule
to take the logging disabled by default for every test, and to have information technology enabled simply for tests annotated with the @LogTiming
notation.
First, we create the new notation:
note class LogTiming
So we can update the LogTimingRule
in this manner:
class LogTimingRule : TestRule { override fun apply ( statement : Argument , clarification : Description ): Statement { return object : Statement () { override fun evaluate () { var enabled = clarification . annotations . filterIsInstance < LogTiming >() . isNotEmpty () val startTime = System . currentTimeMillis () effort { statement . evaluate () } finally { if ( enabled ) { val endTime = Arrangement . currentTimeMillis () println ( "${clarification.methodName} took ${endTime - startTime} ms)" ) } } } } } }
The employ
function is pretty similar to before. We simply added an enabled
flag to check if we should print the fourth dimension or not. The interesting part is patently this:
var enabled = description . annotations . filterIsInstance < LogTiming >() . isNotEmpty ()
Here we go all the annotations from the description
field, we filter them getting only the LogTiming
with the filterIsInstance
method, and we set up the enabled
flag to true if the issue is not empty (please note that also the @Test
annotation will be within the .annotations
collection).
At present we tin just use our note on top of our examination to enable logging merely for the tests we are interested in!
class MySampleTest { @ get : Dominion val dominion = LogTimingRule () @Test @LogTiming fun testSomething () { assertEquals ( 2 , ane + 1 ) } }
A Existent-globe example
Let's meet a real-globe example of a JUnit Rule. This rule will retry failed tests a number of times provided inside an notation on top of the test. The idea behind this Dominion is to mitigate the bear upon of flaky tests.
Flaky tests are tests that can either pass or neglect on the same lawmaking, given the aforementioned configuration/condition.
Flaky tests tin can exist really abrasive, especially when you accept several tests and your test suite takes several minutes to re-run. Ideally, yous would beloved to avoid flakiness at all only is not always possible (east.g. on Android sometimes is really difficult). With this Rule, you can annotate the tests yous know equally being flakier and they volition re-run a defined amount of fourth dimension if they neglect.
Allow's kickoff equally before, with an annotation. This time nosotros also want to pass a parameter, the retryCount
:
annotation course RetryOnFailure ( val retryCount : Int )
This time we need an integer value inside the Dominion to count how many times we demand to retry the exam:
val retryCount = clarification . annotations . filterIsInstance < RetryOnFailure >() . firstOrNull () ?. retryCount ?: 0
Over again we filter the annotations getting only the RetryOnFailure
and nosotros get the first effect. If the consequence is missing, the firstOrNull
method will return cipher and thanks to the elvis operator (?:
) we'll default the value to 0.
And so nosotros tin try to run the examination retryCount + one
times till we become a success:
repeat ( retryCount + 1 ) { _ -> runCatching { statement . evaluate () } . onSuccess { render } . onFailure { failureCause = information technology } }
As soon as nosotros get a success nosotros render. If we get a failure, we store the Throwable
as we desire to print it later if the exam fails:
println ( "Exam ${description.methodName} - Giving up after ${retryCount + 1} attemps" ) failureCause ?. printStackTrace ()
The complete code of the RetryRule is here:
class RetryRule : TestRule { override fun apply ( statement : Argument , description : Description ): Statement { return object : Statement () { override fun evaluate () { val retryCount = description . annotations . filterIsInstance < RetryOnFailure >() . firstOrNull () ?. retryCount ?: 0 var failureCause : Throwable ? = zippo repeat ( retryCount + ane ) { _ -> runCatching { statement . evaluate () } . onSuccess { return } . onFailure { failureCause = it } } println ( "Test ${description.methodName} - Giving up after ${retryCount + 1} attemps" ) failureCause ?. printStackTrace () } } } }
Employ the source, Luke!
I've nerveless the code of those rules on a Maven package. You tin can use them simply by calculation this line to your gradle file:
dependencies { // For JUnit Tests testImplementation 'com.ncorti:rules4android:1.0.0' // For Instrumentation Tests androidTestImplementation 'com.ncorti:rules4android:i.0.0' }
And you can discover the source code on GitHub: cortinico/rules4android.
On method execution order
You're probably wondering how your @Dominion
interacts with all the other annotations provided by JUnit: @Earlier
, @Afterward
, @BeforeClass
, @AfterClass
and @ClassRule
. The better way to observe it is just to try:
course OrderTest { companion object { @ become : ClassRule @JvmStatic val printRule = PrintRule ( "@ClassRule" ) @BeforeClass @JvmStatic fun beforeClass () = println ( " @BeforeClass" ) @AfterClass @JvmStatic fun afterClass () = println ( " @AfterClass" ) } @ become : Rule val rule = PrintRule ( " @Dominion" ) @Before fun earlier () = println ( " @Earlier" ) @After fun subsequently () = println ( " @After" ) @Test fun testSometing () = println ( " @Test testSomething" ) @Test fun testSomethingElse () = println ( " @Test testSomethingElse" ) }
So I assume to have a PrintRule
that prints a line before and after the execution of the Argument
. The output on the console is:
@ClassRule before statement @BeforeClass @Rule before statement @Before @Exam testSomething @After @Rule afterward statement @Rule before statement @Earlier @Test testSomethingElse @Afterwards @Rule after statement @AfterClass @ClassRule after argument
So we can plain see that:
-
Class
annotations are executed only once per exam file (as you would expect). -
@Rule
annotations are wrapping the@After
and@Earlier
executions. -
@ClassRule
annotations are wrapping the@AfterClass
and@BeforeClass
executions.
Make certain to empathize the execution society of JUnit methods, in club to don't go mad with debugging. Finally, don't forget that you lot can employ a RuleChain
to combine multiple rules and to define their lodge.
Appendix: On @get:Rule
You're probably also wondering why practise we need to employ @go:Rule
if you're using Kotlin and not just @Rule
as you would practice in Java.
@ become : Dominion val timingRule = LogTimingRule ()
JUnit needs to have access to your dominion, so it needs to be public. If yous remove the @get:
from the notation, the examination runner will fail with:
org.junit.internal.runners.rules.ValidationError: The @Rule 'timingRule' must be public.
This might look weird every bit the timingRule
is actually public. But what is happening is that by default the @Dominion
annotation is practical to the holding target, that is ignored by the JUnit runner. Kotlin allows yous to specify the target of your annotations so in this case we need to specify the target to be the getter.
Alternatively, you tin instruct the compiler to do not generate a property with the @JvmField
annotation:
@Rule @JvmField val timingRule = LogTimingRule ()
In this manner, getters and setters for timingRule
won't exist created and information technology volition be exposed as a field.
Conclusions
Do y'all want to reuse your testing code? Create a JUnit Dominion!
Inheritance here is not generally a good thought. Yous might have the illusion you're reusing lawmaking, but y'all'll probably finish up in problems really presently. In your testing code, prefer composition over inheritance.
Don't be lazy and start writing your Rules today! 💪
If you want to talk more almost testing, you can reach me out as @cortinico on Twitter.
References
- JUnit4 Wiki - Rules
- JUnit4 Wiki - Exam Fixtures
- JUnit API - Rule
Source: https://ncorti.com/blog/junit-rules
Posted by: gomezarefling.blogspot.com
0 Response to "How To Clean Up After All Junit Tests"
Post a Comment