banner



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:

no go please no

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

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel