Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Test-Driven Development in Go

You're reading from   Test-Driven Development in Go A practical guide to writing idiomatic and efficient Go tests through real-world examples

Arrow left icon
Product type Paperback
Published in Apr 2023
Publisher Packt
ISBN-13 9781803247878
Length 342 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Adelina Simion Adelina Simion
Author Profile Icon Adelina Simion
Adelina Simion
Arrow right icon
View More author details
Toc

Table of Contents (18) Chapters Close

Preface 1. Part 1: The Big Picture
2. Chapter 1: Getting to Grips with Test-Driven Development FREE CHAPTER 3. Chapter 2: Unit Testing Essentials 4. Chapter 3: Mocking and Assertion Frameworks 5. Chapter 4: Building Efficient Test Suites 6. Part 2: Integration and End-to-End Testing with TDD
7. Chapter 5: Performing Integration Testing 8. Chapter 6: End-to-End Testing the BookSwap Web Application 9. Chapter 7: Refactoring in Go 10. Chapter 8: Testing Microservice Architectures 11. Part 3: Advanced Testing Techniques
12. Chapter 9: Challenges of Testing Concurrent Code 13. Chapter 10: Testing Edge Cases 14. Chapter 11: Working with Generics 15. Assessments 16. Index 17. Other Books You May Enjoy

Understanding the benefits and use of TDD

With the fundamentals and best practices of TDD in mind, let us have a more in-depth look at the benefits of adopting it as practice in your teams. As Agile working practices are industry standard, we will discuss TDD usage in Agile teams going forward. Incorporating TDD in the development process immediately allows developers to write and maintain their tests more easily, enabling them to detect and fix bugs more easily too.

Pros and cons of using TDD

Figure 1.7 depicts some of the pros and cons of using TDD:

Figure 1.7 – Pros and cons of using TDD

Figure 1.7 – Pros and cons of using TDD

We can expand on these pros and cons highlights:

  • TDD allows the development and testing process to happen at the same time, ensuring that all code is tested from the beginning. While TDD does require writing more code upfront, the written code is immediately covered by tests, and bugs are fixed while relevant code is fresh in developers’ minds. Testing should not be an afterthought and should not be rushed or cut if the implementation is delayed.
  • TDD allows developers to analyze project requirements in detail at the beginning of the sprint. While it does require product managers to establish the details of what needs to be built as part of sprint planning, it also allows developers to give early feedback on what can and cannot be implemented during each sprint.
  • Well-tested code that has been built with TDD can be confidently shipped and changed. Once a code base has an established test suite, developers can confidently change code, knowing that existing functionality will not be broken because test failures would flag any issues before changes are shipped.
  • Finally, the most important pro is that it gives developers ownership of their code quality by making them responsible for both implementation and testing. Writing tests at the same time as code gives developers a short feedback loop on where their code might be faulty, as opposed to shipping a full feature and hearing about where they missed the mark much later.

In my opinion, the most important advantage of using TDD is the increased ownership by developers. The immediate feedback loop allows them to do their best work, while also giving them peace of mind that they have not broken any existing code.

Now that we understand what TDD and its benefits are, let us explore the basic application of TDD to a simple calculator example.

Use case – the simple terminal calculator

This use case will give you a good understanding of the general process we will undertake when testing more advanced examples.

The use case we will look at is the simple terminal calculator. The calculator will run in the terminal and use the standard input to read its parameters. The calculator will only handle two operators and the simple mathematical operations you see in Figure 1.8:

Figure 1.8 – The simple calculator runs in the terminal

Figure 1.8 – The simple calculator runs in the terminal

This functionality is simple, but the calculator should also be able to handle edge cases and other input errors.

Requirements

Agile teams typically write their requirements from the user’s perspective. The requirements of the project are written first in order to capture customer needs and to guide the test cases and implementation of the entire simple calculator project. In Agile teams, requirements go through multiple iterations, with engineering leadership weighing in early to ensure that the required functionality can be delivered.

Users should be able to do the following:

  • Input positive, negative, and zero values using the terminal input. These values should be correctly transformed into numbers.
  • Access the mathematical operations of addition, subtraction, multiplication, and division. These operations should return the correct results for the entire range of inputs.
  • View fractional results rounded up to two decimal places.
  • View user-friendly error messages, guiding them on how to fix their input.

Agile requirements from the perspective of the user

Requirements are used to capture the needs and perspectives of the end user. The requirements set out the precondition, the user actions, and the acceptance criteria. They specify what we should build as well as how to verify the implementation.

Remember that we only specify requirements on a sprint-by-sprint basis. It is an anti-pattern to specify requirements of the entire product upfront, as well as work in the mindset that they cannot change. Software building in Agile is an iterative process.

Architecture

Our simple terminal calculator is small enough to implement in one sprint. We will take our four requirements and translate them into a simple system architecture. The calculator will be downloaded and run by users locally, so we do not need to consider any networking or cloud deployment aspects.

Figure 1.9 shows what the design of the calculator module could look like:

Figure 1.9 – Architecture of the simple terminal calculator

Figure 1.9 – Architecture of the simple terminal calculator

Each of the components of the calculator module has its own, well-defined responsibilities and functionality:

  • The Input parser is in charge of integrating with the terminal input and reading the user input correctly and passing it to the calculator module.
  • The Input validator is in charge of validating the input sent from the Input parser, such as whether the input contains valid numbers and the operators are valid.
  • Once the input is parsed and validated, the Calculator engine takes in the numbers and attempts to find the result of the operation.
  • The Calculator engine then relies on the Result formatter to format the result correctly and print it to the terminal output. In the case of an error, it relies on the Error formatter to produce and print user-friendly errors.

Applying TDD

As described, we will use the red, green, and refactor process to apply TDD to deliver the required user functionality in an iterative manner. Tests are written first, based on the requirements and design of the simple terminal calculator.

An overview of how the process might work for the implementation of the Divide(x,y) function in the calculator engine is demonstrated in Figure 1.10:

Figure 1.10 – The TDD process applied to the calculator engine

Figure 1.10 – The TDD process applied to the calculator engine

This is a small snapshot that demonstrates the steps involved when using TDD:

  1. We begin by writing a simple TestDivide() that arranges two non-zero inputs and writes assertions for dividing them. This is the simplest case that we can implement. Then, we run the test suite to ensure that the newly written TestDivide() is failing.
  2. Now that the test has established the expected behavior, we can begin our implementation of the Divide(x,y) function. We write just enough code to handle the simple case of two non-zero inputs. Then, we run the test suite to verify that the code we have written satisfies the assertions of TestDivide(). All tests should now be passing.
  3. We can now take some time to refactor the existing code that we have written. The newly written code can be cleaned up according to the clean code practices, as well as the TDD best practices that we have discussed. The test suite is run once more to validate that the refactor step has not broken any new or existing tests.
  4. The simplest functionality for the new Divide(x,y) function is now implemented and validated. We can turn to looking at more advanced functionality or edge cases. One such edge case could be handling a zero divisor gracefully. We now add a new test, TestDivide_Zero(), which sets up and asserts the case of a zero divisor. As usual, we run the test suite to ensure that the new TestDivide_Zero() test is failing.
  5. We modify the implementation of Divide(x,y) to handle a zero divisor gracefully and correctly, as established in the calculator requirements (talking to product owners and perhaps even users if necessary). We run the tests again to ensure that all tests are now passing.
  6. Finally, we begin a new round of refactoring, ensuring that code and tests are well written. All tests are run once more to ensure that refactoring has not caused any errors.

TDD is second nature

The development process swaps between writing test code and writing implementation code as many times as required. While it might seem cumbersome at first, swapping between writing test code and implementation code quickly becomes second nature to TDD practitioners.

Always remember to start with a failing test and then write as little code as possible to make the test pass. Optimize your code only in the refactor phase, once you have all functionality working as verified.

We are now familiar with the process of TDD and have looked at how to write and structure our tests accordingly. However, it’s important to consider alternative processes as well.

You have been reading a chapter from
Test-Driven Development in Go
Published in: Apr 2023
Publisher: Packt
ISBN-13: 9781803247878
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image