Back to Garden
essay

User Driven Development (My take on TDD)

Why adopting a product mindset in your development techniques can make all the difference.

https://peppesilletti.io/content/user-driven-development-my-take-on-tdd/

Adopting a product mindset isn’t just about what you build—it’s about how you approach the process.

Test-Driven Development (TDD) is more than a technique: it’s a philosophy that prioritizes usability, clarity, and user needs from the outset. By focusing on the end-user, whether a colleague, customer, or your future self, TDD ensures code is not only functional but also intuitive and maintainable.

Let’s explore how TDD can help you design better APIs, reduce waste, and improve the quality of your systems.

Test Example Driven Development

TDD is a software design technique that has been created and sponsored by the mighty Kent Beck, one of the 17 original signatories of the Agile Manifesto.

TDD is quite straightforward, and it’s best explained through an example.

Let’s say we want to develop a function to calculate an employee’s salary by counting the days worked, taking into consideration the paid holidays taken and sick days used. How would we go about that?

Let’s put ourselves in the shoes of our function’s users. How would they want to use that function? Let’s write an example:

const employee = {
  salaryPerDay: 100,
  daysWorked: 250,
  holidaysTaken: 15,
  sickDaysUsed: 10,
};

const yearlySalary = calculateEmployeeSalary(employee);
console.log(yearlySalary); // 27300

Is this function any good? Maybe, let’s say it is. TDD puts you in the right mindset to create a user-friendly, maintainable design. And when I say “user” I mean your fellow colleague or yourself in 2 years.

Now, this code is going to fail of course, because the function doesn’t exist yet.

ReferenceError: calculateEmployeeSalary is not defined
    at <anonymous>:8:20
    at dn (<anonymous>:16:5449)

One of the secret ingredients of TDD is making very small steps. Always ask yourself, what’s the smallest thing I could do to get some progress? For the code above, we just need to create the function signature, so to solve the error.

function calculateEmployeeSalary(employee) {}

When I execute the code I now get:

undefined;

Which is fine, because the function is not implemented, yet. We’re expecting the console to print 27300, what’s the smallest step we can take to make it happen? Simple.

function calculateEmployeeSalary(employee) {
  return 27300;
}

You’ll be thinking, well, thanks a lot for that! I want you to trust me, this will make more sense later in the article. It’s always about making small steps.

Thanks for reading Letters From The Build Zone! Subscribe for free to receive new posts and support my work.

Subscribed

Now it’s time to implement our function. The next small step we can take is handling the case with no holidays or sick days taken. We can do this with a new example case.

const employee = {
  salaryPerDay: 100,
  daysWorked: 250,
  holidaysTaken: 0,
  sickDaysUsed: 0,
};

const yearlySalary = calculateEmployeeSalary(employee);
console.log(yearlySalary); // 25000
function calculateEmployeeSalary(employee) {
  let totalSalary = 0;

  totalSalary += employee.salaryPerDay * employee.daysWorked;

  return totalSalary;
}

And now with less than 40 holidays taken:

const employee = {
  salaryPerDay: 100,
  daysWorked: 250,
  holidaysTaken: 15,
  sickDaysUsed: 0,
};

const yearlySalary = calculateEmployeeSalary(employee);
console.log(yearlySalary); // 26500
function calculateEmployeeSalary(employee) {
  let totalSalary = 0;

  totalSalary += employee.salaryPerDay * employee.daysWorked;
  totalSalary += employee.salaryPerDay * employee.holidaysTaken;

  return totalSalary;
}

And so on, you can keep making small steps until you get the full function:

function calculateEmployeeSalary(employee) {
  let totalSalary = 0;

  totalSalary += employee.salaryPerDay * employee.daysWorked;

  if (employee.holidaysTaken > 40) {
    totalSalary -= employee.salaryPerDay * (employee.holidaysTaken - 40);
  } else {
    totalSalary += employee.salaryPerDay * employee.holidaysTaken;
  }

  totalSalary += employee.salaryPerDay * employee.sickDaysUsed * 0.8;

  return totalSalary;
}

You may want to test the function with different combinations of data, so as to hit all the logic branches. In the example above, we’re in a situation where the employee has taken less than 40 holidays. Let’s create an example for the opposite situation.

const employee = {
  salaryPerDay: 100,
  daysWorked: 250,
  holidaysTaken: 50,
  sickDaysUsed: 10,
};

const yearlySalary = calculateEmployeeSalary(employee);
console.log(yearlySalary); // 29800

But wait… it prints 24800 instead of 29800 🤔 ops!

The problem is that even if the holidays taken are more than 40, only the excess is subtracted from the total salary, but the initial 40 paid holidays are skipped.

Let’s fix that:

function calculateEmployeeSalary(employee) {
  let totalSalary = 0;

  totalSalary +=
    employee.salaryPerDay * (employee.daysWorked + employee.holidaysTaken);

  if (employee.holidaysTaken > 40) {
    totalSalary -= employee.salaryPerDay * (employee.holidaysTaken - 40);
  }

  totalSalary += employee.salaryPerDay * employee.sickDaysUsed * 0.8;

  return totalSalary;
}

Now it works! We also have to run again the first example where the employees have taken less than 40 holidays. And da daaa, we get 27300.

Our job is not done, you can continue testing for edge cases, but I’ll keep them out for the sake of simplicity and to not bore you to death.

Something that I’ve done differently here compared to a more “traditional” way of programming, is starting from the example, and not from the implementation. Starting from the implementation may lead to bad design because we’re ignoring the users of the code and the context within the function is being created.

Thanks for reading Letters From The Build Zone! Subscribe for free to receive new posts and support my work.

Subscribed

Example Test Driven Development

TDD will require us to create a test that automates the execution of the examples we wrote above so that you don’t have to run them manually every time. The test framework you’ll using is most likely capable of watching changes for the files you’re testing and re-executing them accordingly. Here’s an example of how the tests could look like. (I’m using the node-tap framework for Node)

test("calculateEmployeeSalary - correct salary calculation with less 40 holidays", (t) => {
  const employee = {
    salaryPerDay: 100,
    daysWorked: 250,
    holidaysTaken: 15,
    sickDaysUsed: 10,
  };

  const expectedResult = 27300;
  const actualResult = calculateEmployeeSalary(employee);
  t.equal(actualResult, expectedResult);
  t.end();
});

test("calculateEmployeeSalary - correct salary calculation with over 40 holidays", (t) => {
  const employee = {
    salaryPerDay: 100,
    daysWorked: 250,
    holidaysTaken: 50,
    sickDaysUsed: 10,
  };

  const expectedResult = 29800;
  const actualResult = calculateEmployeeSalary(employee);
  t.equal(actualResult, expectedResult);
  t.end();
});

The next steps are the same as above, but this time we start from a broken test, then we try to make it pass by working on the function implementation in small steps and without caring much about clean code. Once you get it right, it’s time to refactor it.

This is the famous red, green, refactor cycle that TDD lovers like to brag about.

Note that TDD can be extended for any component of the system you work on. For example, I use it extensively to design WEB APIs using “component testing” (you can find an example here) (create something, get it, updated it, get it again to check the update, delete it, get it again to check it’s not there). You can also use it to design the API of UI components.

TestUser Driven Development

TDD is not a testing strategy, but a design strategy. Tests are nice side effects and they are not to be thrown away, but the real protagonist is the users. For users I mean:

  • The product’s users
  • The code’s users
  • The API’s users
  • The business

We design the external APIs of our system by interacting with them as if we were the user of the product.

We design the internal APIs of our system by thinking about how easily they would be understood by our fellow colleagues or by ourselves in the future.

We design our system to have a testable architecture so that it can be maintained easily and avoid the business wasting time and money on chasing bugs.

TDD isn’t just about writing tests—it’s a design philosophy that fosters better code usability, maintainability, and alignment with user needs. Whether designing APIs or complex architectures, the core principle remains: start from the user’s perspective. Embrace TDD as a tool to reduce waste, deliver value faster, and build systems that truly serve their users.

Is it a surprise that we develop code for someone else, and not for us?