logo
Interview
Unit Testing Interview Questions
Are you the one pushing the code coverage to the limit?
unittesting
Question 1
How much of our code should be covered with unit tests?
There is no exact and universal number as the target. Each team has to decide what is the range of coverage percentage that they should aim for and try to keep that as the application grows.

Ideally that range should be between 50-75%.

It is better to have less tests but of high quality rather than a 100% coverage full of lousy ones.
  • Comprehensive support for parameterized tests
  • Repeated tests
  • Conditional test execution (based on OS, Java version, environment variables etc.)
  • Tagging and filtering
Standard Assertion
Assertion performed after the SUT method has finished its execution.
No assertions are performed before the call to the SUT method.


  // Act
  Integer result = sut.process();

  // Assert
  assertThat(result).isEqualTo(0);



Guard Assertion
Assertion performed just before the SUT method is called.
Its goal is to make sure that all crucial conditions and state are met before the SUT is called.


  assertThat(sut.getState()).isEqualTo(State.READY);

  // Act
  Integer result = sut.process();

  // Assert
  assertThat(result).isEqualTo(0);

Delta Assertion
Consists of the initial assertion performed just before the SUT method is invoked and followed by another one just after which makes sure that the value checked initially has increased / decreased accordingly


  assertThat(list.size()).isEqualTo(initialSize);

  // Act
  list.add(item);

  // Assert
  assertThat(list.size()).isEqualTo(initialSize+1);

Behaviour Verification
Useful when a mocking framework has been used in order not to call a real implementation of one of the dependencies.
Should be used on commands (void methods) in order to verify that they have been called certain number of times and with given set of parameters


  // Arrange
  sut.setCollaborator(Mockito.mock(Collaborator.class));

  // Act
  Integer result = sut.process();

  // Assert
  Mockito.verify(collaborator, times(1)).command(paramOne)

It is strictly forbidden to test private methods.

Private methods are an implementation detail and are really part of one of the public methods. At these we should be aiming in our tests.

Yes, there are reflection libraries and mocking libraries like PowerMock that provide an easy way to test them. This does not mean that we should use them. There is time and place for that but these are very rare cases that occur during legacy code testing.

static Stream<Arguments> shouldSucceed_givenMultiArgumentsProvider() {
    return Stream.of(
        arguments(1, "Frodo", Arrays.asList("abc", "b")),
        arguments(10, "Sauron", Arrays.asList("xyz", "abc"))
    );
}

@ParameterizedTest
@MethodSource("shouldSucceed_givenMultiArgumentsProvider")
void shouldSucceed_givenMultiArguments(int index, String str, List<String> list){
  assertThat(str.length).isGreaterOrEqualTo(5);
  assertThat(index).isPositive();
  assertThat(list).contains("abc");
}


This is a tricky question. A unit test should in general stay away from invoking most of the collaborators logic.

It should especially avoid:
  • Hitting the database
  • Reading / Writing to a file system
  • Connecting to a network
  • Invoking threads

Only trivial collaborator methods are allowed to be invoked as they are. Anything more and we are stepping into an integration test territory.
@BeforeAll
Executed before each @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory method in the current class.

@BeforeEach
Executed before all @Test, @RepeatedTest, @ParameterizedTest, and @TestFactory methods in the current class.

@AfterEach
Executed after each @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory method in the current class.

@AfterEach
Executed after all @Test, @RepeatedTest, @ParameterizedTest, and @TestFactory methods in the current class

There are almost always three parts to each of the test methods:
  • In first part we are preparing all the needed objects and setting all the values that are needed to successfully perform given test
  • In the second part we simply call the SUT's method
  • Finally we assert values and verify behaviour that we expect to occur.


Most of the time we would be using the following two strategies to structure our tests:

  public void shouldDoX() throws Exception{
    // Arrange

    // Act

    // Assert
  }


  public void shouldDoX() throws Exception{
    // Given

    // When

    // Then
  }

  • Hamcrest
  • Assertj

They provide way more descriptive assertions.
On top of that the error messages we get can be much more detailed. Most of the times we can detail them even more.

Here is an example of an Assertj assertion:

  // Assert
  assertThat("Jamaica").startsWith("Jam")
                           .endsWith("ca")
                           .isEqualToIgnoringCase("jamaicA");

Happy Path
The way a functionality was meant to work from start to finish. Also defined as a default or positive behaviour. Nothing unexpected happens here.

Edge Case
When doing edge cases testing we are literally going to the borders of a certain feature and often way beyond it. Any of the following could be describe as an edge case:

  • A method accepts Integers in the range of <-100,100>. An edge case would test the method passing in -100 and 100 values respectively.
  • There is a division in a certain method. We pass in 0 to see how our feature handle's an arithmetic exception.
  • We try to invoke a recursive method in a way that it has a very large number of nested calls. Way more than expected. Will this be handled or we are going to get a stack overflow exception?

Unless your team lead is a complete pessimist and always sees things going wrong then I guess you could go for the edge and error cases first just to make him happy... and .. serene.

Besides that, in most of the cases we should always strive to test the happy path first. This is the path for which our feature was built and would be the most common and often one. Also if we ran out time all of a sudden and we were able to write only a handful of tests, we at least know that the most important path works as it should.
We are always striving to test the behaviour of the SUT, not its methods.

When we name our tests based on the behaviour of the SUT, we are writing a short story documenting what is the SUT all about. We do not even need to analyze its code when we first stumble upon it in order to get the understating of how it works from way above.

Also, method names might change and that will not be handled by our IDE in cases of test renaming. We would simply end up with tests method names that are completely out of tune in regards to the SUT's changed naming.