navigation

Unit Testing in Java – The Missing Bits (Part 1)

Unit Testing in Java – The Missing Bits (Part 1)

by
September 14, 2021
frontpage, Java
No Comment

Greetings! 

Are you stuck on unit testing? So was I. 

I was looking for unit testing rules a while ago. Something to live by. Somewhere to start. I managed to find quite a few resources about unit testing itself, what is a mock, what is a stub, how and when to use them and so on… but… I still felt something was missing. 

And I found what it was! I didn’t know HOW to write my unit tests. Unit tests happened to follow a bit different set of rules compared to production code. Rules that lie below.

So here it is. The missing bits. Years of gathered hands-on experience and knowledge from different sources, people, projects and teams. It is about time this became available. At your disposal. 

Special thanks to Denis Danov, who was so kind to let me use a big part of his resources for this article.

Feel free to bookmark this article and come back to it every time you feel stuck. At least this is what I do during my daily work at Dreamix.

My Part in Fighting The Big Three

Starting from the basics. The three hardest things in software development are:

  1. Naming things
  2. Cache invalidation

Oh, and off-by-one errors.

Here is how we can fight item #1. Some function naming standards. Choose wisely.

  • operation_whenConditions_generatesResult
  • Example: runEngine_withWrongFuel_fails
  • operation_shouldGenerateResult_whenCondition_isPresent
  • Example: runEngine_shouldFail_whenWrongFuel_isProvided
  • resultOccurs_ifCondition_isPresent
  • Example: engineFails_ifWrongFuel_isProvided

Good practices

Structuring tests

Unit tests have three mandatory parts:

  1. Given 
  2. When 
  3. Then 

OR triple A (AAA):

  1. Arrange 
  2. Act 
  3. Assert

Separating code visually (through adding blank lines) into these three parts provides easier understanding, better readability of the tests and ensures we are not missing something important.

Examples can be found here.

Given / Arrange

This part is the preparation and it contains the input data for the test. All variables, literals and mocked behavior is here. You can further separate this part into three more sections:

  1. Mocks initialization
  2. Data (real objects) initialization
  3. Mocking of desired behavior (e.g. by using Mockito)

When / Act

Here resides the actual execution that we will test. Usually a call of a function on the object that is under test.

This is the “operation” from the naming standards above.

Then / Assert

Here we validate that the behavior has produced the desired outcome.

A test without validation on the generated results tests nothing, just increases test coverage. It is meaningless and provides no value.

Important note to architects and managers: 

If you enforce high test coverage passing gates for developers, they might start skipping this part of the tests, thus achieving the desired coverage BUT without providing any substantial value to your project.

More on Naming, Readability and Test Structure

  • Use “objectUnderTest” or “componentUnderTest” for the name of the object that you are testing
  • Why: Improves readability of tests, easier focus on what is important.
  • Example: Car objectUnderTest = new MercedesBenz();
  • Use builder pattern for constructing complex objects
  • Why: Name of fields are obvious and values are understandable.
  • Example: Car objectUnderTest = MercedesBenzBuilder.oneBenz().withFuel(GAZOLINE).withTankCapacity(80L).build();
  • Use variables for values that do not provide enough context (empty string, literal numbers, nulls, etc.)
  • Why: Improves readability and understanding of the test case.
  • Example: long tankCapacityInLitres = 80L;
  • Extract variables/constant fields for literals when a specific value is “the same” (as a meaning) as other specific value for the scope of the same test
  • Why: Helps to make the literals more readable and understandable in context of purpose.
  • A single test should test one thing only and should fail for a single reason
  • Why: This is why it is a unit test. It tests a unit in isolation. It should be pure. It should not test interaction between different components. It should not face any unwanted side effects.
  • Compare whole object instead of separate internal values
  • Why: Allows for a single assert. You will be asserting a single thing in the test
  • Note: This does not mean it is forbidden to have multiple asserts. Experience and context will also guide you.
  • Add “Mock” at end of mocked variable names to separate logically from real object instances
  • Why: It allows for easier code reviews. Improves readability. Allows you to focus faster on the important parts.
  • Example: Engine gazolineEngineMock = mock(GazolineEngine.class);
  • Note: When you have to mock multiple dependencies of the same type you will need to stick to their original field names in the object under test (in case you use @InjectMocks)
  • (JUnit 5) You can use @DisplayName on test classes to provide a custom name in test results
  • Why: Human readable test results.
  • Add human-readable text to failing assertions (especially for true/false assertions)
  • Why: Faster understanding of what and why breaks.

Some Testing Principles

  • Strive your test to not be validation of your implementation, but rather a validation of the desired unit behaviour 
  • Why: You want to validate behavior, not implementation. Otherwize it would lead to fragile tests that result in “change-detecting” tests. You do NOT want that.
  • Rules:

■ if a method returns result, validate the result

■ if a method does not return a result – you can verify the called methods of mocked dependencies (you may have no other way to verify the behavior)

  • Why (Red): Always see your test failing at least once (prior to newly introduced code). This is one way you can trust your tests are not false positives (at the time of their creation)
  • Why (Refactor): Care for your tests as you care for your production code. Remove any redundant mocks, tests that are not valid anymore, tests that duplicate test scenarios…
  • While following the test pyramid try to have a failing unit test for each integration test that fails
  • Why: Enables easier and faster localization of the exact source of problems

Read Part 2 here!

About the author

Martin Patsov is a Java Expert at Dreamix, a custom software development company. He has rich experience in software development with Java & Spring, Angular, SQL, JUnit, communication with clients and management of small teams. 

His personal motto is to “Think (before you act & about improvements), Create (your own world, your dreams & the world around you) and Share (the knowledge, feedback & motivation)”. One of his habits is to always go the extra mile. His motivation is to: “Be the person you needed when you were younger.”

vvvvvvvvvv

Think. 

Create. 

Share

Your turn. 

^^^^^^^^^^^

You can contact and follow Martin via:

martin.patsov@dreamix.eu 

Martin Patsov

Java Expert at Dreamix

More Posts - Website

Follow Me:
LinkedIn

Do you want more great blogs like this?

Subscribe for Dreamix Blog now!