Home Web Development Test-Driven Development in Go

Test-Driven Development in Go

By Adelina Simion
ai-assist-svg-icon Book + AI Assistant
eBook + AI Assistant $31.99 $21.99
Print $39.99
Subscription $15.99 $10 p/m for three months
ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription.
ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription. $10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime! ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription.
What do you get with a Packt Subscription?
Gain access to our AI Assistant (beta) for an exclusive selection of 500 books, available during your subscription period. Enjoy a personalized, interactive, and narrative experience to engage with the book content on a deeper level.
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
Gain access to our AI Assistant (beta) for an exclusive selection of 500 books, available during your subscription period. Enjoy a personalized, interactive, and narrative experience to engage with the book content on a deeper level.
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Along with your eBook purchase, enjoy AI Assistant (beta) access in our online reader for a personalized, interactive reading experience.
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription. ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription. BUY NOW $10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime! ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription.
eBook + AI Assistant $31.99 $21.99
Print $39.99
Subscription $15.99 $10 p/m for three months
What do you get with a Packt Subscription?
Gain access to our AI Assistant (beta) for an exclusive selection of 500 books, available during your subscription period. Enjoy a personalized, interactive, and narrative experience to engage with the book content on a deeper level.
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
Gain access to our AI Assistant (beta) for an exclusive selection of 500 books, available during your subscription period. Enjoy a personalized, interactive, and narrative experience to engage with the book content on a deeper level.
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Along with your eBook purchase, enjoy AI Assistant (beta) access in our online reader for a personalized, interactive reading experience.
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
  1. Free Chapter
    Chapter 1: Getting to Grips with Test-Driven Development
About this book
Experienced developers understand the importance of designing a comprehensive testing strategy to ensure efficient shipping and maintaining services in production. This book shows you how to utilize test-driven development (TDD), a widely adopted industry practice, for testing your Go apps at different levels. You’ll also explore challenges faced in testing concurrent code, and learn how to leverage generics and write fuzz tests. The book begins by teaching you how to use TDD to tackle various problems, from simple mathematical functions to web apps. You’ll then learn how to structure and run your unit tests using Go’s standard testing library, and explore two popular testing frameworks, Testify and Ginkgo. You’ll also implement test suites using table-driven testing, a popular Go technique. As you advance, you’ll write and run behavior-driven development (BDD) tests using Ginkgo and Godog. Finally, you’ll explore the tricky aspects of implementing and testing TDD in production, such as refactoring your code and testing microservices architecture with contract testing implemented with Pact. All these techniques will be demonstrated using an example REST API, as well as smaller bespoke code examples. By the end of this book, you’ll have learned how to design and implement a comprehensive testing strategy for your Go applications and microservices architecture.
Publication date:
April 2023
Publisher
Packt
Pages
342
ISBN
9781803247878

 

Getting to Grips with Test-Driven Development

Programs and software have never been more complex than they are today. From my experience, the typical tech startup setup involves deployment to the cloud, distributed databases, and a variety of software integrations from the very beginning of the project. As we use software and consume data at unprecedented rates, the expectation of high availability and scalability has become standard for all the services we interact with.

So, why should we care about testing when we are so busy delivering complex functionality in fast-paced, high-growth environments? Simply put, to verify and prove that the code you write behaves and performs to the expectations and requirements of your project. This is important to you as the software professional, as well as to your team and product manager.

In this chapter, we’ll look at the Agile technique of Test-Driven Development (TDD) and how we can use it to verify production code. TDD puts test writing before implementation, ensuring that test scripts cover and change with requirements. Its techniques allow us to deliver quality, well-tested, and maintainable code. The task of software testing is a necessity for all programmers, and TDD seamlessly incorporates test writing into the code delivery process.

This chapter begins our exploration into the world of testing. It will give you the required understanding of TDD and its main techniques. Defining and setting these fundamentals firmly in our minds will set the stage for the later implementation of automated testing in Go.

In this chapter, we’ll cover the following main topics:

  • The world and fundamentals of TDD
  • The benefits and use of TDD
  • Alternatives to TDD
  • Test metrics
 

Exploring the world of TDD

In a nutshell, TDD is a technique that allows us to write automated tests with short feedback loops. It is an iterative process that incorporates testing into the software development process, allowing developers to use the same techniques for writing their tests as they use for writing production code.

TDD was created as an Agile working practice, as it allows teams to deliver code in an iterative process, consisting of writing functional code, verifying new code with tests, and iteratively refactoring new code, if required.

Introduction to the Agile methodology

This precursor to the Agile movement was the waterfall methodology, which was the most popular project management technique. This process involves delivering software projects in stages, with work starting on each stage once the stage before it is completed, just like water flows downstream. Figure 1.1 shows the five stages of the waterfall methodology:

Figure 1.1 – The five stages of the waterfall methodology

Figure 1.1 – The five stages of the waterfall methodology

Intuition from manufacturing and construction projects might suggest that it is natural to divide the software delivery process into sequential phases, gathering and formulating all requirements at the start of the project. However, this way of working poses three difficulties when used to deliver large software projects:

  • Changing the course of the project or requirements is difficult. A working solution is only available at the end of the process, requiring verification of a large deliverable. Testing an entire project is much more difficult than testing small deliverables.
  • Customers need to decide all of their requirements in detail at the beginning of the project. The waterfall allows for minimal customer involvement, as they are only consulted in the requirements and verification phases.
  • The process requires detailed documentation, which specifies both requirements and the software design approach. Crucially, the project documentation includes timelines and estimates that the clients need to approve prior to project initiation.

The waterfall model is all about planning work

Project management with the waterfall methodology allows you to plan your project in well-defined, linear phases. This approach is intuitive and suitable for projects with clearly defined goals and boundaries. In practice, however, the waterfall model lacks the flexibility and iterative approach required for delivering complex software projects.

A better way of working named Agile emerged, which could address the challenges of the waterfall methodology. TDD relies on the principles of the Agile methodology. The literature on Agile working practices is extensive, so we won’t be looking at Agile in detail, but a brief understanding of the origins of TDD will allow us to understand its approach and get into its mindset.

Agile software development is an umbrella term for multiple code delivery and project planning practices such as SCRUM, Kanban, Extreme Programming (XP), and TDD.

As implied by its name, it is all about the ability to respond and adapt to change. One of the main disadvantages of the waterfall way of working was its inflexibility, and Agile was designed to address this issue.

The Agile manifesto was written and signed by 17 software engineering leaders and pioneers in 2001. It outlines the 4 core values and 12 central principles of Agile. The manifesto is available freely at agilemanifesto.org.

The four core Agile values highlight the spirit of the movement:

  • Individuals and interactions over processes and tools: This means that the team involved in the delivery of the project is more important than their technical tools and processes.
  • Working software over comprehensive documentation: This means that delivering working functionality to customers is the number one priority. While documentation is important, teams should always focus on consistently delivering value.
  • Customer collaboration over contract negotiation: This means that customers should be involved in a feedback loop over the lifetime of the project, ensuring that the project and work continue to deliver value and satisfy their needs and requirements.
  • Responding to change over following a plan: This means that teams should be responsive to change over following a predefined plan or roadmap. The team should be able to pivot and change direction whenever required.

Agile is all about people

The Agile methodology is not a prescriptive list of practices. It is all about teams working together to overcome uncertainty and change during the life cycle of a project. Agile teams are interdisciplinary, consisting of engineers, software testing professionals, product managers, and more. This ensures that the team members with a variety of skills collaborate to deliver the software project as a whole.

Unlike the waterfall model, the stages of the Agile software delivery methodology repeat, focusing on delivering software in small increments or iterations, as opposed to the big deliverables of waterfall. In Agile nomenclature, these iterations are called sprints.

Figure 1.2 depicts the stages of Agile project delivery:

Figure 1.2 – The stages of Agile software delivery

Figure 1.2 – The stages of Agile software delivery

Let’s look at the cyclical stages of Agile software delivery:

  1. We begin with the Plan phase. The product owner discusses project requirements that will be delivered in the current sprint with key stakeholders. The outcome of this phase is the prioritized list of client requirements that will be implemented in this sprint.
  2. Once the requirements and scope of the project are settled, the Design phase begins. This phase involves both technical architecture design, as well as UI/UX design. This phase builds on the requirements from the Plan phase.
  3. Next, the Implement phase begins. The designs are used as the guide from which we implement the scoped functionality. Since the sprint is short, if any discrepancies are found during implementation, then the team can easily move to earlier phases.
  4. As soon as a deliverable is complete, the Test phase begins. The Test phase runs almost concurrently with the Implement phase, as test specifications can be written as soon as the Design phase is completed. A deliverable cannot be considered finished until its tests have passed. Work can move back and forth between the Implement and Test phases, as the engineers fix any identified defects.
  5. Finally, once all testing and implementation are completed successfully, the Release phase begins. This phase completes any client-facing documentation or release notes. At the end of this phase, the sprint is considered closed. A new sprint can begin, following the same cycle.

The customer gets a new deliverable at the end of each sprint, enabling them to see whether the product still suits their requirements and inform changes for future sprints. The deliverable of each sprint is tested before it is released, ensuring that later sprints do not break existing functionality and deliver new functionality. The scope and effort of the testing performed are limited to exercising the functionality developed during the sprint.

One of the signatories of the Agile manifesto was software engineer Kent Beck. He is credited with having rediscovered and formalized the methodology of TDD.

Since then, Agile has been highly successful for many teams, becoming an industry standard because it enables them to verify functionality as it is being delivered. It combines testing with software delivery and refactoring, removing the separation between the code writing and testing process, and shortening the feedback loop between the engineering team and the customer requirements. This shorter loop is the principle that gives flexibility to Agile.

We will focus on learning how to leverage its process and techniques in our own Go projects throughout the chapters of this book.

Types of automated tests

Automated testing suites are tests that involve tools and frameworks to verify the behavior of software systems. They provide a repeatable way of performing the verification of system requirements. They are the norm for Agile teams, who must test their systems after each sprint and release to ensure that new functionality is shipped without disrupting old/existing functionality.

All automated tests define their inputs and expected outputs according to the requirements of the system under test. We will divide them into several types of tests according to three criteria:

  • The amount of knowledge they have of the system
  • The type of requirement they verify
  • The scope of the functionality they cover

Each test we will study will be described according to these three traits.

System knowledge

As you can see in Figure 1.3, automated tests can be divided into three categories according to how much internal knowledge they have of the system they test:

Figure 1.3 – Types of tests according to system knowledge

Figure 1.3 – Types of tests according to system knowledge

Let’s explore the three categories of tests further:

  • Black box tests are run from the perspective of the user. The internals of the system are treated as unknown by the test writer, as they would be to a user. Tests and expected outputs are formulated according to the requirement they verify. Black box tests tend not to be brittle if the internals of the system change.
  • White box tests are run from the perspective of the developer. The internals of the system are fully known to the test writer, most likely a developer. These tests can be more detailed and potentially uncover hidden errors that black box testing cannot discover. White box tests are often brittle if the internals of the system change.
  • Gray box tests are a mixture of black box and white box tests. The internals of the system are partially known to the test writer, as they would be to a specialist or privileged user. These tests can verify more advanced use cases and requirements than black box tests (for example security or certain non-functional requirements) and are usually more time-consuming to write and run as well.

Requirement types

In general, we should provide tests that verify both the functionality and usability of a system.

For example, we could have all the correct functionality on a page, but if it takes 5+ seconds to load, users will abandon it. In this case, the system is functional, but it does not satisfy your customers’ needs.

We can further divide our automated tests into two categories, based on the type of requirement that they verify:

  • Functional tests: These tests cover the functionality of the system under test added during the sprint, with functional tests from prior sprints ensuring that there are no regressions in functionality in later sprints. These kinds of tests are usually black box tests, as these tests should be written and run according to the functionality that a typical user has access to.
  • Non-functional tests: These tests cover all the aspects of the system that are not covered by functional requirements but affect the user experience and functioning of the system. These tests cover aspects such as performance, usability, and security aspects. These kinds of tests are usually white-box tests, as they usually need to be formulated according to implementation details.

Correctness and usability testing

Tests that verify the correctness of the system are known as functional tests, while tests that verify the usability and performance of the system are known as non-functional tests. Common non-functional tests are performance tests, load tests, and security tests.

The testing pyramid

An important concept of testing in Agile is the testing pyramid. It lays out the types of automated tests that should be included in the automated testing suites of software systems. It provides guidance on the sequence and priority of each type of test to perform in order to ensure that new functionality is shipped with a proportionate amount of testing effort and without disrupting old/existing functionality.

Figure 1.4 presents the testing pyramid with its three types of tests: unit tests, integration tests, and end-to-end tests:

Figure 1.4 – The testing pyramid and its components

Figure 1.4 – The testing pyramid and its components

Each type of test can then be further described according to the three established traits of system knowledge, requirement type, and testing scope.

Unit tests

At the bottom of the testing pyramid, we have unit tests. They are presented at the bottom because they are the most numerous. They have a small testing scope, covering the functionality of individual components under a variety of conditions. Good unit tests should be tested in isolation from other components so that we can fully control the test environment and setup.

Since the number of unit tests increases as new features are added to the code, they need to be robust and fast to execute. Typically, test suites are run with each code change, so they need to provide feedback to engineers quickly.

Unit tests have been traditionally thought of as white-box tests since they are typically written by developers who know all the implementation details of the component. However, Go unit tests usually only test the exported/public functionality of the package. This brings them closer to gray-box tests.

We will explore unit tests further in Chapter 2, Unit Testing Essentials.

Integration tests

In the middle of the testing pyramid, we have integration tests. They are an essential part of the pyramid, but they should not be as numerous and should not be run as often as unit tests, which are at the bottom of the pyramid.

Unit tests verify that a single piece of functionality is working correctly, while integration tests extend the scope and test the communication between multiple components. These components can be external or internal to the system – a database, an external API, or another microservice in the system. Often, integration tests run in dedicated environments, which allows us to separate production and test data as well as reduce costs.

Integration tests could be black-box tests or gray-box tests. If the tests cover external APIs and customer-facing functionality, they can be categorized as black-box tests, while more specialized security or performance tests would be considered gray-box tests.

We will explore integration tests further in Chapter 4, Building Efficient Test Suites.

End-to-end tests

At the top of the testing pyramid, we have end-to-end tests. They are the least numerous of all the tests we have seen so far. They test the entire functionality of the application (as added during each sprint), ensuring that the project deliverables are working according to requirements and can potentially be shipped at the conclusion of a given sprint.

These tests can be the most time-consuming to write, maintain, and run since they can involve a large variety of scenarios. Just like integration tests, they are also typically run in dedicated environments that mimic production environments.

There are a lot of similarities between integration tests and end-to-end tests, especially in microservice architectures where one service’s end-to-end functionality involves integration with another service’s end-to-end functionality.

We will explore end-to-end tests further in Chapter 5, Performing Integration Testing, and Chapter 8, Testing Microservice Architectures.

Now that we understand the different types of automated tests, it’s time to look at how we can leverage the Agile practice of TDD to implement them alongside our code. TDD will help us write well-tested code that delivers all the components of the testing pyramid.

The iterative approach of TDD

As we’ve mentioned before, TDD is an Agile practice that will be the focus of our exploration. The principle of TDD is simple: write the unit tests for a piece of functionality before implementing it.

TDD brings the testing process together with the implementation process, ensuring that every piece of functionality is tested as soon as it is written, making the software development process iterative, and giving developers quick feedback.

Figure 1.5 demonstrates the steps of the TDD process, known as the red, green, and refactor process:

Figure 1.5 – The steps of TDD

Figure 1.5 – The steps of TDD

Let’s have a look at the cyclical phases of the TDD working process:

  1. We start at the red phase. We begin by considering what we want to test and translating this requirement into a test. Some requirements may be made up of several smaller requirements: at this point, we test only the first small requirement. This test will fail until the new functionality is implemented, giving a name to the red phase. The failing test is key because we want to ensure that the test will fail reliably regardless of what code we write.
  2. Next, we move to the green phase. We swap from test code to implementation, writing just enough code as required to make the failing test pass. The code does not need to be perfect or optimal, but it should be correct enough for the test to pass. It should focus on the requirement tested by the previously written failing test.
  3. Finally, we move to the refactor phase. This phase is all about cleaning up both the implementation and the test code, removing duplication, and optimizing our solution.
  4. We repeat this process until all the requirements are tested and implemented and all tests pass. The developer frequently swaps between testing and implementing code, extending functionality and tests accordingly.

That’s all there is to doing TDD!

TDD is all about developers

TDD is a developer-centric process where unit tests are written before implementation. Developers first write a failing test. Then, they write the simplest implementation to make the test pass. Finally, once the functionality is implemented and working as expected, they can refactor the code and test as needed. The process is repeated as many times as necessary. No piece of code or functionality is written without corresponding tests.

TDD best practices

The red, green, and refactor approach to TDD is simple, yet very powerful. While the process is simple, we can make some recommendations and best practices for how to write components and tests that can more easily be delivered with TDD.

Structure your tests

We can formulate a shared, repeatable, test structure to make tests more readable and maintainable. Figure 1.6 depicts the Arrange-Act-Assert (AAA) pattern that is often used with TDD:

Figure 1.6 – The steps of the Arrange-Act-Assert pattern

Figure 1.6 – The steps of the Arrange-Act-Assert pattern

The AAA pattern describes how to structure tests in a uniform manner:

  1. We begin with the Arrange step, which is the setup part of the test. This is when we set up the Unit Under Test (UUT) and all of the dependencies that it requires during setup. We also set up the inputs and the preconditions used by the test scenario in this section.
  2. Next, the Act step is where we perform the actions specified by the test scenario. Depending on the type of test that we are implementing, this could simply be invoking a function, an external API, or even a database function. This step uses the preconditions and inputs defined in the Arrange step.
  3. Finally, the Assert step is where we confirm that the UUT behaves according to requirements. This step compares the output from the UUT with the expected output, as defined by the requirements.
  4. If the Assert step shows that the actual output from the UUT is not as expected, then the test is considered failed and the test is finished.
  5. If the Assert step shows that the actual output from the UUT is as expected, then we have two options: one option is that if there are no more test steps, the test is considered passed and the test is finished. The other option is that if there are more test steps, then we go back to the Act step and continue.
  6. The Act and Assert steps can be repeated as many times as necessary for your test scenario. However, you should avoid writing lengthy, complicated tests. This is described further in the best practices throughout this section.

Your team can leverage test helpers and frameworks to minimize setup and assertion code duplication. Using the AAA pattern will help to set the standard for how tests should be written and read, minimizing the cognitive load of new and existing team members and improving the maintainability of the code base.

Control scope

As we have seen, the scope of your test depends on the type of test you are writing. Regardless of the type of test, you should strive to restrict the functionality of your components and the assertions of your tests as much as possible. This is possible with TDD, which allows us to test and implement code at the same time.

Keeping things as simple as possible immediately brings some advantages:

  • Easier debugging in the case of failures
  • Easier to maintain and adjust tests when the Arrange and Assert steps are simple
  • Faster execution time of tests, especially with the ability to run tests in parallel

Test outputs, not implementation

As we have seen from the previous definitions of tests, they are all about defining inputs and expected outputs. As developers who know implementation details, it can be tempting to add assertions that verify the inner workings of the UUT.

However, this is an anti-pattern that results in a tight coupling between the test and the implementation. Once tests are aware of implementation details, they need to be changed together with code changes. Therefore, when structuring tests, it is important to focus on testing externally visible outputs, not implementation details.

Keep tests independent

Tests are typically organized in test suites, which cover a variety of scenarios and requirements. While these test suites allow developers to leverage shared functionality, tests should run independently of each other.

Tests should start from a pre-defined and repeatable starting state that does not change with the number of runs and order of execution. Setup and clean-up code ensures that the starting point and end state of each test is as expected.

It is, therefore, best that tests create their own UUT against which to run modifications and verifications, as opposed to sharing one with other tests. Overall, this will ensure that your test suites are robust and can be run in parallel.

Adopting TDD and its best practices allows Agile teams to deliver well-tested code that is easy to maintain and modify. This is one of many benefits of TDD, which we will continue to explore in the next section.

 

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.

 

Alternatives to TDD

As we’ve seen, TDD is simply a way to deliver well-tested code in an iterative way. Putting tests first ensures that no functionality is ever delivered without being tested and refactored. In this section, we will have a look at some other common processes for testing code.

Waterfall testing

As we remember from our introduction to the waterfall methodology, the testing or verification phase of waterfall projects happens after the implementation phase is fully completed. The entire project is delivered, and all requirements are implemented by this point.

Here are the advantages:

  • Waterfall projects are typically well structured and well documented. Testing plans are informed by this extensive documentation and testers can ensure that all of the end-to-end tests that they implement cover the identified user needs.
  • Developers and testers can rely on the project documentation to work independently, without the need to communicate. This division allows teams to work in shifts – testers verify functionality and developers fix any bugs that may arise.

These are the disadvantages:

  • As the entire project is already implemented, it is easier for bugs to become complex. Furthermore, since the entire project is already implemented, it might take considerably more engineering effort to fix a bug, in the case that large changes need to be undertaken.
  • In the case that client requirements are not well known or clear from the beginning, a lot of implementation and testing effort might be wasted if the requirements change once the client sees the delivered product at the end of the process.
  • The testing process can often be seen as a time-wasting, negative exercise that should be finished as soon as possible. Furthermore, if there are delays in the development process, it can be easy to cut corners in the verification process, delivering an unstable product.

Acceptance Test-Driven Development

Acceptance Test-Driven Development (ATDD) is an Agile development process related to TDD. ATDD involves people from multiple disciplines from product, engineering, and testing to ensure that the right product is being developed in the right way. The customer requirements are translated into a list of requirements that can be understood by a wide variety of stakeholders. These requirements are then converted to automated acceptance tests, which are used to verify what the engineering department is delivering.

The advantages of ATDD are as follows:

  • Just like with TDD, tests are written first when you use ATDD. A complete suite of automated acceptance tests can be run after each commit or incremental code delivery, ensuring that all end-to-end functionality works as expected.
  • If done right, using ATDD on a project will be widely supported by a wide variety of stakeholders inside the business, as they will have a good understanding of the direction and customer value it will provide.

The disadvantages are as follows:

  • Significant communication and synchronization effort is required for the inter-disciplinary effort of writing requirements. It can be time-consuming to get a variety of stakeholders to give the time and effort needed.
  • This approach might not be best suited for greenfield projects, where there are a lot of unknowns upfront. It can be particularly challenging to write acceptance tests for a project that does not even have an API or database model yet.
  • It can be challenging to get sample payloads or datasets from the outset of a project, especially if these are provided by the client or a third party.

Further related to ATDD, we have Behavior-Driven Development (BDD). It provides precise guidance on how to structure the conversation between stakeholders using business domain language. We will explore BDD further in Chapter 5, Performing Integration Testing.

As we begin to write and think of test code together with functional code, it’s important to set success criteria for our test code. Test metrics can help us achieve just that.

 

Understanding test metrics

Now that we understand how to deliver projects with tests first, it’s time to look at some metrics that can quantify how well-tested a project is. It’s important to deliver tests across the entire test pyramid, as it’s important to be able to ensure the application is working correctly end-to-end as well as working well with its external dependencies.

Important test metrics

There is a wide range of metrics that we can measure when quantifying the quality of software:

  • Requirement coverage: This indicates the percentage of your project requirements that are covered by tests. A test could cover multiple requirements, but no customer requirement should be left untested.
  • Defect count and distribution: This indicates how many defects or bugs are discovered in each module/part of the application. The distribution will also signal whether there are any particular problem areas in the system that could be refactored.
  • Defect resolution time: This indicates how quickly the development team is able to fix bugs once they are detected. A long Mean Time To Resolution (MTTR) can indicate that the development team is short-staffed, while a long max resolution time in a particular area of the system can indicate that the code in that particular part is difficult to change.
  • Code coverage: This indicates the percentage of your code base that is exercised by unit tests. Since tests should be written first, coverage also shows whether the development team is using TDD. Low test coverage can also indicate issues with the system design.
  • Burndown rates and charts: These indicate the rate at which the team is able to deliver functionality. As development and testing are a unified task, a user story or requirement cannot be considered complete unless it is tested, so the burndown rate will include only stories that are ready for delivery. Burndown charts can indicate delays in project timelines.

Code coverage

Since the code coverage metric is such an important TDD indicator, let’s explore it further. In order to achieve a high coverage percentage, tests should cover the following:

  • The functions you implemented
  • The statements that your functions are composed of
  • The different execution paths of your functions
  • The different conditions of your Boolean variables
  • The different parameter values that can be passed to your functions

The Go test runner provides the coverage percentage for Go applications. We will have a look at how to do this in Chapter 2, Unit Testing Essentials.

Figure 1.11 shows a flow chart of the implementation of the Divide(x,y) function from the simple terminal calculator:

Figure 1.11 – Execution flow of the Divide function in the simple calculator

Figure 1.11 – Execution flow of the Divide function in the simple calculator

Tests should be written to cover and verify the following:

  • The execution path for y != 0
  • The execution path for y == 0
  • The error message of the DivideZero error
  • The output from the result calculation statements
  • The output from the print result statements

Code coverage percentage

In large projects, it will be unfeasible to reach 100% test coverage for the code base. There have been many discussions in the tech community about what a good test coverage percentage is. It is generally accepted that a good coverage amount is around the 80% mark. After that point, experience shows there can be diminishing returns.

The code coverage percentage will also depend on the kind of project you are running. A legacy code base with a low code coverage percentage will require considerable effort to bring up to the 80% mark. Similarly, a greenfield project will also be difficult to test if there are many unknowns.

Just like any code you write, test code needs to be maintained and refactored. Keeping a high coverage percentage requires maintaining and updating a lot of test code. This can increase the development cost of refactoring or other code changes, as it potentially requires updating many test cases. The business value of maintaining tests that do not cover requirements is very low, so you should ensure that your tests are providing value to your test suites.

Well-tested code is not necessarily bug-free code

Your tests should aim to provide verification for important code behavior, as opposed to simply writing code to get a certain code coverage percentage. The team should embrace a testing culture using TDD and a good coverage percentage will follow.

 

Summary

In this chapter, we covered all the testing fundamentals that you will need to get started with TDD. We began with an explanation of what Agile is and how TDD fits into the Agile development process. You learned about the different types of automated tests and the testing pyramid. Then, we looked at the iterative process of delivering code with TDD using the red, green, and refactor process, and explored some TDD best practices on how to structure and write tests.

In Chapter 2, Unit Testing Essentials, we will learn how to write tests in Go and begin to get some hands-on experience with TDD. We will begin to use the red, green, and refactor process and write tests according to the TDD best practices that we have learned.

 

Questions

  1. What is the testing pyramid? What are its components?
  2. What is the difference between functional and non-functional tests?
  3. Explain what the red, green, and refactor TDD approach is.
  4. What is ATDD?
  5. What is code coverage?
 

Further reading

  • Learning Agile: Understanding Scrum, XP, Lean, and Kanban – Andrew Stellman and Jennifer Greene, published by O’Reilly Media
  • Test Driven Development: By Example – Kent Beck, published by Addison-Wesley Signature Series
  • Clean Code: A Handbook of Agile Software Craftsmanship – Robert C. Martin, published by Prentice Hall
 

Answers

  1. The testing pyramid specifies how automated test suites should be structured. At the bottom of the pyramid are unit tests, which test a single isolated component. Next up in the middle of the pyramid are integration tests, which test that multiple components are able to work together as specified. Finally, at the top of the test pyramid are end-to-end tests that test the behavior of the entire application.
  2. Functional tests cover the correctness of a system, while non-functional tests cover the usability and performance of a system. Both types of tests are required to ensure that the system satisfies the customers’ needs.
  3. The red, green, and refactor TDD approach refers to the three phases of the process. The red phase involves writing a new failing test for the functionality we intend to implement. The green phase involves writing enough implementation code to make all tests pass. Finally, the refactor phase involves optimizing both implementation and testing code to remove duplication and come up with better solutions.
  4. Acceptance test-driven development. Just like TDD, ATDD puts tests first. ATDD is related to TDD, but it involves writing a suite of acceptance tests before the implementation begins. It involves multiple stakeholders to ensure that the acceptance test captures the customer’s requirements.
  5. Code coverage is the percentage of your lines of code that are exercised by your unit test. This is calculated by considering the function statements, parameter values, and execution paths of your code. The Go test runner outputs the calculated code coverage. We should aim for a good value, but optimizing for 100% is normally not appropriate.
About the Author
  • Adelina Simion

    Adelina Simion is a technology evangelist at Form3. She is a polyglot engineer and developer relations professional, with a decade of technical experience at multiple start-ups in London. She started her career as a Java backend engineer, converted later to Go, and then transitioned to a full-time developer relations role. She has published multiple online courses about Go on the LinkedIn Learning platform, helping thousands of developers upskill with Go. She has a passion for public speaking, having presented on cloud architectures at major European conferences. Adelina holds an M.Sc. in mathematical modeling and computing.

    Browse publications by this author
Test-Driven Development in Go
Unlock this book and the full library FREE for 7 days
Start now