logo
Articleoodesign

Test method naming

Views: 914 Created: 2019-06-22 Read time: 7 minutes
Post Preview
Tags:

1)     Preface

Sometimes the hard part is not the content of an automated test itself but a meaningful and descriptive name. If you think about it, how can you write a high-quality test if you are not 100% sure what case should it validate? The title should explain that with great clarity. If it doesn't, you do not know what you really should do inside.

We will go through some of the best practices and tips regarding the naming conventions for unit testing, and not only.  

 

2)     Unit test naming and BDD

There is nothing special about unit tests. Each and everyone should look more or less the same. Here is an example structure:

 

Test Code


@Test
public void should<Result>_when<Action>_given<Input/State>() throws Exception{
    // Given

    // When

    // Then
}

 

A good practice is to always put the throws clause at the method definition because a test method is not a place for taking care of production code exceptions that are not expected by the test itself.

 

When it comes to the method name, it consists of :

       Given (optional): An optional part defining the initial state/input before the main feature is called.

       When(optional): defines the action/feature under test.

       Should: determines what should be outcome/state after the tested method has been invoked.

 

This structure follows the Behavioral Driven Design approach. We aim for expressing the feature of the class here, written in a way that introduces a certain level of abstraction. Don't focus on communicating how the method under test works precisely as we might introduce unnecessary implementation details.

 

Tip
Tip

 

Note
Note

 

3)     Unit tests as a living documentation of the code

I am a firm believer that more comments you have in your code the more trouble you are trying get yourself into. Yes, I am talking mostly about the interface comments. Did you have inline comments in mind? This word should not even be in your dictionary.

A lot of times, we change the implementation, but we forget to update the comments. Then we simply end up with different code and description. That is bad, and it continually happens in every application. There is a straightforward remedy that reduces the need for comments or even makes them obsolete altogether. That is following the Single Responsibility Principle which enforces the creation of small, specialised units and unit testing.

Once we cover our SUT with a suite of precise, descriptive unit tests, we really do not need any comments. When we want to understand the code, we simply go to the test class and finger through the test names. This should give us a much better overview than any comments. We can even debug our suite to get into even more detail. Commenting does not have this feature yet, unfortunately.

 

Note
Note

 

Tip
Tip

 

4)     Unit Test naming: the example

Enough talk, let's go through an example SUT where we want to add a bonus buff for our hero:

 

SUT Code


public void gainBuff(Hero hero, BuffType buffType){
	if(buffType == null){
		throw new RuntimeException("You have to assign something bro.");
	}

	if(hero.getActiveBuff() != null){
		return;
	}

	if(hero.getMoraleLevel().equals(Moral.HIGHEST)){
		hero.setActiveBuff(new SuperBuff("super duper buff", buffType));

		return;
	}

	hero.setActiveBuff(new Buff("normal duper buff", buffType));
}

Quite a few things are happening here. Let's try to write a couple of possible test names:

 

Test Code


@Test
public void shouldKeepActiveBuff(){

@Test
public void shouldGainNormalBuff_givenWeakHeroMorale(){

@Test
public void shouldGainSuperBuff_givenHighHeroMorale(){

@Test
public void shouldThrowException_whenGainingBuff_givenNoBuffProvided(){

As we can see with these three methods, we have covered 100% of the logic of our SUT method. We have also created names that are not too long but still describe the test accordingly. We have omitted the when part a few times but it is always optional, and the should part encapsulates it a lot of the time.

 

5)     Unit Test naming: a hard case

There is a very unique symbiosis between software craftsmanship and unit testing (automated testing in general also). When it comes to software craftsmanship, we are making sure that technically everything is written in a way that guarantees the highest quality of code. When it comes to automated testing, we are making sure that we consistently get the highest quality in terms of functionality. Combine them together, and your software is literally unstoppable. 

A lot of times unit testing can act as a watchdog of the technical quality of our application. Let us look at the following example of a restaurant management application where the customer tries to pay for his order:

 

SUT Code


public static Bill processBill(Customer customer, Waiter waiter, Order order){
	Bill bill = waiter.prepareBillForOrder(order);
	Iterator<CreditCard> creditCards = customer.prepareToPay();

	while(!bill.paid()){
		if(creditCards.hasNext()){
			CreditCard creditCard = creditCards.next();

			TransactionResult transactionResult = TransactionResult.WRONG_PIN;

			while(TransactionResult.WRONG_PIN.eqauls(transactionResult)){
				transactionResult = waiter.performTransaction(bill, creditCard);

				if(transactionResult == TransactionResult.WRONG_PIN){
					waiter.informPinNotAccepted(customer, bill);
				}
			}

			if(transactionSuccessful){
				bill.setPaid(true);
				break;
			}else{
				waiter.informTransactionNotAccepted(customer, bill);
			}
		}else{
			throw new RuntimeException("Customer cannot pay");
		}
	}

	return bill;
}

Now let's try to think how could we name unit tests that would be verifying this method. We want it to comply with the BDD principle:

 

Test Code


@Test
void shouldWaiterProcessCustomerOrder_givenFirstTransactionAccepted()

@Test
void shouldWaiterRepeatCardTransaction_whenProcessingCustomerOrder_givenWrongPinHasBeenProvidedForCard()

@Test
void shouldWaiterInformTransactionNoAccepted_whenProcessingCustomerBill_givenFirstCardTransactionUnsuccessful()

@Test
void shouldThrowException_whenProcessingCustomerBillByWaiter_givenNoneOfTheCustomerCardTransactionsWasSuccessfull();

These do not look good, do not sound right, and possibly they even do not smell that good either. Our method definitely tries to do too much. We simply cannot come up with meaningful unit test names that are not a mile long. That is the indicator. We lost contact with SRP here. Let's try to refactor a bit the method and think about the test names once again:

 

Refactored Code


public Bill processOrder(Customer customer, Order order){
	Bill bill = waiter.prepareBillForOrder(order);
	Iterator<CreditCard> creditCards = customer.prepareToPay();

	while(!bill.paid()){
		if(creditCards.hasNext()){
			bill.setPaid(payForBill(creditCards.next(), customer));
		}else{
			throw new RuntimeException("Customer cannot pay");
		}
	}

	return bill;
}

public boolean payForBill(CreditCard creditCard, Bill bill){
	TransactionResult transactionResult = TransactionResult.WRONG_PIN;

	while(TransactionResult.WRONG_PIN.eqauls(transactionResult)){
		transactionResult = performTransaction(bill, creditCard);

		if(transactionResult == TransactionResult.WRONG_PIN){
			informPinNotAccepted(customer, bill);
		}
	}

	if(TransactionResult.SUCCESSFUL.equals(transactionResult)){
		bill.setPaid(true);
		return true;
	}else{
		informTransactionNotAccepted(customer, bill);
	}

	return false;
}

We have extracted the card processing details and moved the methods to the Waiter class. Thanks to that, we will not need to use that word in our tests as the context will be obvious. Thanks to refactoring, we have two specialised methods instead of one. Now we can write unit tests for these separately. Let's he how it went:

 

Test Code


@Test
void shouldRepeatCardTransaction_givenWrongPinHasBeenProvided()

@Test
void shouldInformTransactionNotAccepted()

@Test
void shouldMarkTheBillAsPaid_givenCardTransactionSuccessful()

 

Test Code


@Test
void shouldProcessOrder_givenFirstTransactionAccepted()

@Test
void shouldProcessOrder_givenSubsequentTransactionAccepted()

@Test
void shouldThrowException_whenProcessingCustomerOrder_givenNoCustomerCardTransactionsWasSuccessfull();

 

This looks a lot better. Both the refactored method and the unit test naming. I think we are quite there on the right path. Some of the methods might seem still a bit long, though. Do you think we should refactor even further?

 

Tip
Tip

 

6)     Integration Test naming

On a unit level, it is not that hard to come up with some proper names for your tests. Even if you need to refactor a bit to get them just right.

On an IT level, though, things might seem a bit daunting at first. IT's are not testing isolated units, they are testing much, much more. A lot of times external systems are involved etc. How can I explain all those conditional paths along the way with just a few words? It is impossible, right?

Wrong. It is actually precisely the same process as unit test naming most of the time. Remember, we are aiming at BEHAVIOUR here, not implementation. Once we get focused on implementation we are doomed on any automation level. We will go through an example where will be trying to save a game state though a REST API:

 

SUT Code


@RequestMapping(method = RequestMethod.POST, path="/state/save")
public HttpStatus saveGameState(Hero hero, Map map){
	if(map.previouslySaved()){
		log.error("Can save only once on given map");

		return HttpStatus.BAD_REQUEST;
	}

	if(!validateHeroData(hero)){
		log.error("The data seems to be malformed or hacked.");

		return HttpStatus.BAD_REQUEST;
	}

	gameStateService.save(hero,map);

	return HttpStatus.OK;
}

We assume this integration test has access to all external resources. Exactly like it would run on a production environment. If we forget about the network layer, the database layer, all the intricacies and logic of gameStateService.save and focus on the behaviour, there are really just a couple of things going on a high abstraction level:

 

Test Code


@Test
@Tag("end-to-end")
public void shouldSaveGameState()

@Test
@Tag("end-to-end")
public void shouldResultInBadRequest_whenSavingGameState_givenInvalidHeroData()

@Test
@Tag("end-to-end")
public void shouldResultInBadRequest_whenSavingGame_givenInvalidCurrentState()

We simply have to cover the border cases separately and write an adequate test for the happy path. That's it. If we want to get into details of the behaviour of the save we should create separate slice tests. These would cover only portion of the flow. For example interaction between two components only or one element and database.

 

Note
Note

 

 

7)     Conclusion

 

Summary

 

Test names are living watchdogs for the complexity of our solution and whether we are trying to test too much at the same time. Test names should be as descriptive as possible, while at the same time not looking like a novel. And remember, think of behaviours!

 

 

Need more insight?
Repository
Repository
Glossary
Glossary
Tags:
Reference
You may also like:
junit5-tags-and-filters
JUnit 5 tags and filters
basics-of-unit-testing
Basics of unit testing
Comments
Be the first to comment.
Leave a comment