3 Steps Of Test Driven Development That Help You Build Better Code Faster

Jakub Sobolewski
3 min readOct 11, 2023

Test Driven Development is a design practice that has a tremendous effect on the quality of the software we’re building.

  • ⌛️ It provides instant feedback on the design of our code.
  • 🧱 It helps us build modular code, with good separation of concerns.
  • 🌱 It makes us grow software in many small steps, limiting the risk we’re building the wrong thing.

And all it takes is to stick to these 3 steps.

🔴 1. Write the test first and see that it fails.

This step is key to the whole practice, if we skip it, we miss out on all of its benefits!

Setting up a test and seeing it fail is crucial. We need to make sure the test has the ability to fail if the code doesn’t meet the requirements. Contrary to writing tests after code, when we write tests to make them pass. Starting with a test helps us focus on asserting the expected behavior of the code, rather than trying to prove that existing code works as expected.

Start with at least one case, and describe what it should do, not how it should do it.

library(testthat)

describe("my_median", {
it("should return a value that separates lower half from higher half of a sample", {
# Arrange
x <- c(1, 2, 3)
# Act
result <- my_median(x)
# Assert
expect_equal(result, 2)
})
})

🟢 2. Write code to pass the test.

Once we have at least one test in place, and we confirmed that it fails, we can write code that will make the test pass.

Having tests in place allows you to quickly re-run them. Each time you write some code, you can see if it meets the requirements. No more selecting batches of code in the editor, running it, and checking it manually. We can check all specifications at once, reproducibly. No more accidental breaking of previously implemented behavior when we add a new one.

We should write only as much code as is needed to pass tests — why write more code if less already satisfies all requirements? Don’t write more code — You Ain’t Gonna Need It!

my_median <- function(x) {
n <- length(x)
sorted <- sort(x)
sorted[(n + 1) / 2]
}

This implementation will pass the test because so far we only expect the median function to work on an odd length vector.

That’s how we end up with incremental design — we can implement behaviors one by one, in small steps — testing if median works for even length vectors or vectors with missing values is a job for additional test cases.

♻️ 3. Refactor to improve the design.

This step involves improving both the design and implementation of our code.

It’s the time to make the code more beautiful, efficient and robust. We may modify the internals of the code however we want, as long it passes our tests. No more fright that refactoring will break something! This is also the step when we reevaluate our design choices, for example we may notice that the code we implemented doesn’t handle missing values. This may lead to a choice to introduce a parameter that controls whether the function should ignore missing values or not.

Use this step to reevaluate your design choices, make the code simpler and more robust.

--

--

Jakub Sobolewski
Jakub Sobolewski

Written by Jakub Sobolewski

I help R developers get better at testing | Tech Lead @ Appsilon

No responses yet