logo
Solutionunittest

Mockito argument capturing

Views: 182 Created: 2019-05-02 Read time: 4 minutes
Post Preview
Tags:

1)     Preface

ArgumentCaptor allows us to capture arguments that were passed to our stubs cleanly and simply.

This feature is particularly useful when we would like to inspect in more detail the passed object.

This is because we would like to check something more than just its type or that it is equal to another object.

Taking that into consideration, it might be stated that ArgumentCaptor is strongly related to the custom argument matchers.

In our test method we can create an ArgumentCaptor as follows:


final ArgumentCaptor<Person> personCaptor = ArgumentCaptor.forClass(Person.class);

Another way would be by using an annotation:


@Captor
private ArgumentCaptor<Person> personCaptor;

ArgumentCaptor has its limitations though. It enables the compile-time checks, but during runtime, it may get a bit fuzzy.

 

Tip
Tip

 

2)     Mockito ArgumentCaptor: capturing single argument

Let's go through an example where we will be capturing a single argument. In this scenario our Hero will be trying to win a fight with a strong enemy!

SUT Code


public void fightTheBoss(Hero hero, Boss boss){
    gameEngine.castSpell(hero.mostPowerfullOffensiveSpell(), boss);

    while(boss.isAlive()){
        boolean criticalHit = gameEngine.determineCritical(hero.getEquippedWeapon(), hero);

        AttackOutcome attackOutcome = gameEngine.attack(
                hero.getEquippedWeapon(), boss, criticalHit);

        if(attackOutcome.isDeadly()){
            boss.setAlive(false);
        }
    }
}

In this SUT method we are:

      (L5):  Check whether the boss has been killed. If not we continue to fight.

      (L6):  Based on weapon stats and hero state, we determine whether the next hit will be a critical one.

      (L8):  We attack the boss.

      (L11):  If the attack was deadly we mark the boss as killed.

Now we want to make sure that the last hit that killed the boss has been critical. That is just how our game should work. You could not kill a boss if the last hit was not critical.

Test Code


@Test
public void shouldKillBossWithCriticalHit() throws Exception{
    // Arrange
    Hero hero = // prepare Hero;
    Boss boss = // prepare Boss;

    AttackOutcome normalAttackOutcome = new AttackOutcome();
    AttackOutcome finalAttackOutcome = new AttackOutcome();
    finalAttackOutcome.setDeadly(true);

    when(gameEngineMock.determineCritical(weapon,hero))
            .thenReturn(false)
            .thenReturn(false)
            .thenReturn(true);

    when(gameEngineMock.attack(eq(weapon), eq(boss), anyBoolean()))
            .thenReturn(normalAttackOutcome)
            .thenReturn(normalAttackOutcome)
            .thenReturn(finalAttackOutcome);

    // Act
    gameControllerSUT.fightTheBoss(hero, boss);

    // Assert
    ArgumentCaptor<Boolean> criticalHitCaptor = ArgumentCaptor.forClass(Boolean.class);

    verify(gameEngineMock, times(3))
		.attack(eq(weapon), eq(boss), criticalHitCaptor.capture());

    assertThat(criticalHitCaptor.getValue()).isTrue();
}

 In this test method we:

      (L12):  Setting the gameEngineMock to generate a critical hit on the third attempt.

      (L17):  Setting the gameEngineMock to generate a fatal attack on the third attempt

      (L28):  We capture the crticialHit outcome that was passed to the attack method. The criticalHitCaptor.capture() will capture the last argument that was passed. This is exactly what we need here.

      (L31):  We make sure that the last hit that killed the boss was actually critical.

 

Note
Note

 

3)     Mockito @Captor: capturing multiple arguments

Let's take a closer look at an example where more than one argument is being captured. This time our hero will be trying to open a freshly found loot chest!

SUT Code


public void openLootChest(Hero hero, Chest chest){
    for(Lockpick lockpick: hero.getLockpicks()){
        boolean opened = gameEngine.attemptToOpen(lockpick
                , hero.getLockpickingLevel(), chest.getPercentageChanceToSpawnGuardian());

        hero.increaseLockpicking();
        chest.increaseChanceToSpawnGuardian();

        if(opened){
            hero.addMoney(chest.getMoney());
            return;
        }
    }
}

In this SUT method we are:

      (L3):  Attempting to open the chest until the hero has no more Lockpics.

      L(7,8): After each attempt, the users lockpicking skill is increased. Also, a chance to spawn a chest guardian is increased.

Let's now try to write a test that makes sure that after each attempt:

      A new Lockpick is used.

      Heroes lockpicking skill is increased.

      Chest's guardian spawn chance is increased.

Test Code


@Captor private ArgumentCaptor<Lockpick> lockpickCaptor;
@Captor private ArgumentCaptor<Integer> lockpickLevelCaptor;
@Captor private ArgumentCaptor<Integer> guardianChanceCaptor;

@Test
public void shouldOpenLootAfterMultiAttempt() throws Exception{
    // Arrange
	when(gameEngineMock.attemptToOpen(any(Lockpick.class), anyInt(), anyInt()))
            .thenReturn(false)
            .thenReturn(false)
            .thenReturn(true);

    // Act
    gameControllerSUT.openLootChest(new Hero(), new Chest());

    // Assert
    verify(gameEngineMock, times(3)).attemptToOpen(lockpickCaptor.capture()
        , lockpickLevelCaptor.capture()
        , guardianChanceCaptor.capture());

    List<Lockpick> lockpicks = lockpickCaptor.getAllValues();
    List<Integer> lockpickLevels = lockpickLevelCaptor.getAllValues();
    List<Integer> guardianChances = guardianChanceCaptor.getAllValues();

    assertThat(new HashSet<>(lockpicks).size()).isEqualTo(3);
    assertThat(Ordering.natural().isOrdered(lockpickLevels)).isTrue();
    assertThat(Ordering.natural().isOrdered(guardianChances)).isTrue();
}

 In this test code we are:

      (L2,3,4):  Define the captors with the @Captor annotation.

      (L9):  Set up the gameEngineMock to open the chest after the third attempt.

      (L18):  We verify that the attemptToOpen method has been called 3 times. We also capture all the three arguments collections as they were passed along.

      (L26):  We assert that each of the lockpics was unique.

      (L27,28):  We assert that after each attempt the hero.lockpickingLevel and chest.guardianSpawnChance have been incremented.

 

Note
Note

 

4)     Mockito ArgumentCaptor: there are no type checks

In the Javadocs for this feature, it is clearly stated that it does not perform any type checks during runtime and it is only there to avoid casting in the code.

Let's look at a particular case:

SUT Code


public interface CarServisable{

	public void service(Car car);

Now let's create a test where we invoke the method twice with a different subclass of the accepted argument type:

Test Code


@Test
public void shouldServiceMultipleCars() throws Exception{
    // Arrange
	CarServicable serviceMock = mock(CarServicable.class);

    Ferrari ferrari = new Ferrari(500); // horse power
	AstonMartin astonMartin = new AstonMartin(700); // horse power

    // Act
	serviceMock.service(ferrari);
	serviceMock.service(astonMartin);

    // Assert
    ArgumentCaptor<Ferrari> ferrariCaptor = ArgumentCaptor.forClass(Ferrari.class);
	ArgumentCaptor<AstonMartin> astonMartinCaptor = ArgumentCaptor.forClass(AstonMartin.class);

	verify(serviceMock, times(2)).service(ferrariCaptor.capture());
	verify(serviceMock, times(2)).service(astonMartinCaptor.capture());

	List<Ferrari> ferraris = ferrariCaptor.getAllValues();
	List<AstonMartin> astonMartins = astonMartinCaptor.getAllValues();
}

In this test method we:

      (L7,8):  Create different car instances.

      (L11,12):  Invoke the service method twice. For each passing a different car instance each time.

      (L15,16):  Prepare an ArgumentCaptor for each of the concrete car classes.

      (L18,19):  Verify that service was called twice and capturing the car each time. Note that we need to duplicate the process for each of the car captors.

      L(21,22): Retrieve all the values captured by each of the captors.

Now a funny thing happens when we inspect the content of each of the captors. The ferraris and astonMartins list both contain two elements and both contain a instance of Ferrari and AstonMartin. This is a design flaw of the ArgumentCaptor that is underlined during scenarios like the one above.

The problem is that each of the lists will be the size of 2 all will contain instances of both Ferrari and AstonMartin.

The solution would be to take advantage of the custom ArgumentMatcher feature:

Test Code


// Assert

verify(serviceMock, times(2)).service(argThat((Car car) ->{
    if(car instanceof Ferrari){
        return car.getHorsePower().equals(500);
    }else if(car instanceof AstonMartin){
        return car.getHorsePower().equals(700);
    }

    return false;
}));

Thanks to that we can make sure that each of the Ferrari and AstonMartin instances was passed precisely once and that each car has the specific horsepower.

5)     Conclusion

Summary

 

We have seen that ArgumentMatcher can be a more lightweight alternative to the customer argument matchers.

We just need to watch out for some exceptional cases when it falls short, but if we do, we will be all fine.

 You never know, you might capture something really unexpected!

 

Need more insight?
Repository
Repository
Glossary
Glossary
Tags:
Reference
You may also like:
mockito-verification
Mockito verification
mockito-stubbing
Mockito stubbing
mocking-object-creation-with-powermock
Mocking object creation with PowerMock
Comments
Be the first to comment.
Leave a comment