TDD? A brief overview

Today we'll be briefly exploring Test Driven Development (TDD). What it is, how it came about, and an overview of the various levels of testing you'll find across the developer landscape.

Bugs, bugs, and more bugs.

A developer attempting a bugfix 10 minutes before the beta release deadline.

Historical Background

Fixing bugs tends to be the most costly part of the development process across the entire industry. As such, the industry had developed various methods to combat overhead costs such as agile software development methodologies to optimize the lifecycle of product release iteration to be as fast and bug-free as possible.

QA terms are an answer to this, but it created an inner cycle within the agile process of regression testing. Bouncing fixes back and forth with a QA team until the patch didn't cause any regressions to other features of a product.

But wouldn't it be a lot more easier to hand off a perfect bug-free fix to a QA team than have them check through more extensive and complicated usages of the product rather than narrowly focusing on smaller problems that can be fixed before they see them?

Fortunately through "programming books from the 60s", Kent Beck rediscovered the ancient art of unit testing! It has been an old process started during the time of punchcard programming where developers had limited time to work on machines to run their programs. It allows for a preliminary guarantee that a bug fix is clean it reaches QA testers. It also allows them to focus on the bigger picture and reduce the back and forth overhead of regressing testing.

But how did unit testing develop into TDD?

From Unit Testing into TDD

Unit testing defined in it's most basic form is writing code to test for correct output or procedures for another piece of code. Unit tests gives an insurance that any code written with a test is sure to work given the requirements for the test to pass. A unit test can be as simple as checking the result of an addition function:

// This is the piece of code we'll be testing!
const addTwoNumbers = (x , y) => x + y;

// We expect the result of 2 + 2 to equal 4
// So our test does exactly that to check the result
// of the function.
test("Expect two numbers to add to 4", () => {
  const result = addTwoNumbers(2, 2);
  expect(result).toEqual(4);
}

That is great because we can now know beforehand that our code runs as intended before deploying it to a live application... But what if we wrote a mistake in our test that allows our test to pass regardless of input?

test("Expect two numbers to add to 4", () => {
  const result = addTwoNumbers(4, -2);
  expect(result).toEqual(true);
}

Our tests still pass, but now it's not testing our function's output! Uh oh!

Even with tests, there are still possibilities of human error that could allow bad code to sneak past them. What if addTwoNumbers had a side effect that changed the margin-offset of a button that was formerly associated with that function but now has a different function?

With a test like this, we'll find a visual regression of the button now having a weird offset whenever the addTwoNumbers function fires. Which would've been circumvented if we knew our test tested what we wanted! Any feature additions, logic, or side effects in addTwoNumbers will go unnoticed. That could be a massive issue if addTwoNumbers were a critical part of an application that must be extensively worked on.

So how do we resolve this issue?

What we need to do is establish a process of testing that avoids any possibility of human error while writing our tests and ensure our code does what it needs to do and only that!

An Answer: Test Driven Development

Test Driven Development (TDD for short) is a process that allows us to thoroughly enjoy the benefits of testing without succumbing to the pitfalls of human error. TDD can be summed up as a simple process of 5 steps:

  1. Add a test.
  2. Run the test and make sure it fails. That will tell us two things:

    • The test framework (Could be Jest, Enzyme, or Jasmine.) we are using to write our tests in is working correctly.
    • It ensures that the test won't always pass, avoiding false positives from slipping through, solving the earlier example!
  3. Write the code to make sure the test pasts.
  4. Refactor code your code and continuingly rerun your tests to make sure it still pasts.
  5. Repeat until all requirements are filled!

Thanks to TDD, we can be confident about our code working reliably and is written in the most optimal way possible. It's a simple formula that'll always work as long as we stick to them.

Levels Of Testing

With TDD, we now have a way to test our application without causing human error and allows us to be more confident about the reliability of our code. But as today's applications are growing ever increasingly complex, we need to start leveraging our newfound knowledge to test more broader parts of an application rather than individual pieces of code. We'll the options available to address this issue: Integration Testing, and End to End (E2E) Testing; along with a recap on Unit Testing.

Unit Testing is what we've gone over earlier. What constitutes a "Unit" is usually a single function or self-contained block of code. All forms of software testing are composed of unit tests.

Integration Testing are unit tests combined to execute what could be part of a more complex operation an application needs to do. They are essentially various unit tests executed in order to perform a certain operation such as updating a database and ensuring the response returned comes as expected.

End to End Testing more broadly is all about testing an entire user flow. For example: if we were to do to a E2E test for a eCommence site, a common user flow would be for a customer arriving on the site and purchasing a product. We'd test every piece of software involved in entire process of a user finding a product, adding it to their cart, then purchasing it. This can consists of multiple integration tests working in tandem to map a flow and ensure more broader usages of an application are working.

Conclusion

And that's everything! There's a bit of oversimplification on some aspects discussed here, but generally this is a quick exploration of everything about TDD and common applications of the process to test more abstract and larger parts of an application. I'll probably revise some aspects in this post in future, and explore more about testing software. Hope this helped!