Let’s talk Test Driven Development — how I write tests before writing any real code

Paul Tran
No Moss Co.
Published in
6 min readApr 28, 2023

--

Test Driven Development (TDD) is a great way to reduce waste and build quality digital products. A common question when chatting with an early career developer trying TDD is “How do I write my tests when I don’t even know what code I’m going to write?”

As the Director of Product at No Moss, I relish any time I get to spend “on the tools”, such as getting my hands dirty in code. Recently on a client project, I was lucky enough to work with our talented lead developer, Hugh Blackall, and enjoyed the challenge of writing automated tests before we wrote any code, as well as creating a robust, clean, high quality codebase for an iOS app in a short amount of time (case study incoming!).

So I’m happy to share my process for designing and testing software. Writing tests prior to writing code is very helpful, not just for repeatable tests but also to create well designed software.

I hope you might find the below “How to” useful too. Happy reading!

How to write tests before writing code for TDD

Step 0: Rough out the logic sequence, from brain to paper first

✏️ Key advice: (1) go from beginning to end to get the full picture first, and (2) make it cheap, fast and throwaway (it’s for your eyes only) ✏️

  • Draw and design the sequence that you think is needed to achieve the desired outcome, by sketching the flow on paper or Miro
  • This is especially helpful for a complex flow, if there’s a mix of synchronous and asynchronous flows, or if it’s difficult to figure out where to start in an existing codebase
  • You don’t have to be fancy on what modelling standard you use (C4 or UML). It just needs to make sense to you.
Galaxy brain
If you can diagram in C4, then congratulations you have Galaxy Brain!
  • Optionally, overlay your diagram with the respective user stories to make sure you’ve covered all of your acceptance criteria
A example of a how to diagram tests in Miro
Two examples of how I roughly sketch out the sequence of code, from beginning to end

Step 1: Start with your interface (function signatures) and fill it with pseudocode

✍️ Key advice: Pseudocode doesn’t break — feel free to rough it all out ✍️

Now create a rough pseudocode commented skeleton of how you might code it.

  • This skeleton is a tool for helping your brain sufficiently think through how you might solve this, while not (yet) burning time in trying to make it run.

Here’s an example of what my pseudocode looks like:

   getSupportedTypes(): PersonType[] {
// Get all passages with the XX tag

// Expect to get only one

// Parse the source text and extract the link text and passage id

// Return only 1 person type
throw new Error("Method not implemented.");
}

getSupportedSituations(personId): string): Situation[] {
// Find the passage with the person ID with XX tag

// Parse the source text and extract the link text and passage id

// Return an array
throw new Error("Method not implemented.");
}

getStartingPassage(personId: string, situationId: string): Passage {
// Find the passage matching person id

// Traverse until find scenario id

// Throw exception if can't find or encounter another XX tag

// return the passage that follows the situation id
throw new Error("Method not implemented.");
}

getPassage(id: string): Passage {
// Return lookup from map
throw new Error("Method not implemented.");
}

Step 2: Write your tests!

🕒 Key advice: Start with tests in “pending” state, and steadily fill them out 🕒

  • Hopefully by now you have a rough sense of the functions/methods/components you need to implement, and sequence for how they work together
  • Decide whether your tests should be unit tests, component tests and/or e2e tests
  • Start with the title for each test — don’t fill them out just yet
  • The title should state the expected behaviour
    - If you’re using Cypress, you can define tests as “pending” by not defining the test closure. When you run the test, those tests will show up as “Pending”. E.g: it(“displays correct response based on chosen prompt”);
    - If you’re using Jest, you can define pending tests by using it.todo or test.todo
  • Remember to cover both positive and negative scenarios

My example of a test scaffold:

import { it, describe } from "@jest/globals";

describe("when the user starts the application", () => {
it("provides expected number of supported person types", () => { });
it("has the expected number of adventure for each person", () => {});
});
describe("when the user selects a person and adventure", () => {
it("has the expected adventure for a person", () => {});
it("has a starting story for each person/adventure pair", () => {});
it("provides a valid passage by Id", () => {});
});
  • Once you have your pending tests, start filling them out.
  • If you’re component testing: Cypress pro tip — use data-* attributes when selecting elements with cy.get. This provides context to your selectors and isolates them from CSS or JS changes. It also allows you to pick them without having to worry about carefully naming css class selectors or ids
                       <button
class="prompt-button conversation-regular"
data-cy="prompt-button"
>
{{ link.labelText }}
</button>
       cy.log("TEST: Selecting user typing my own results in keyboard focus");
cy.get("[data-cy=user-typed-option-button]").should("be.visible");
cy.get("[data-cy=user-typed-option-button]").click();
  • And if you’re unit testing: Jest pro tip: Use your roughed out method/function definitions in your test. You can use throw new Error(“Method not implemented.”); to purposefully fail your tests, to remind you to implement them later

Step 3: Run the tests and see them fail. That’s a good thing! Then start coding

🚦Key advice: Use the fail/pass statuses as goalposts🚦

  • Run the test before you start coding. Yes you heard right. See all the lights go red. This is a good thing. This gives you your goal — make all the lights go green
    - Fill out your methods/functions.
    - Replace your pseudo code with your actual implementation
  • Start to feel the juicy dopamine hits as your automated test runs steadily turn green
  • Bask in the glory knowing that every time you yarn test, you are saving hours of rework. No more:
    - Writing dodgy console.log and watching the console to check that its doing what it should be doing
    - Recompiling or re-serving your app to click through again, and again, and again, and again to make sure it’s still working
    - Endless back-and-forth of someone downstream who has stumbled on a bug
  • Eventually all your tests become green! And now there’s one final step…

Step 4: Refactor!

👩‍💻 Key advice: Even if you only spend 5 minutes refactoring, future you will thank you for this. 👩‍💻

  • You’re probably thinking: All my tests passed! So why the heck should I waste time refactoring?
  • The answer is because you’re a professional, thoughtful and kind software engineer that cares about the maintainability of the code and sanity of the next person (including you!) that might have to touch it sometime later
  • The important thing with automated tests is: you can now refactor with confidence
  • You might have once avoided changing anything after your code is ‘done’, lest something suddenly breaks, causing headaches for your colleagues, clients, and yourself
  • Now you can cave into overly obsessing about clean code, or making changes to improve its maintainability and performance
  • Your automated tests will have your back, provided you have written a good coverage of automated tests!

As you can see, Test Driven Development can be a simple and fun process that results in an all round better life for you and your team, and ultimately your end product.

If you’re curious about how this process might fit in with your team to get better results for you, your team and your product, drop me a line for an obligation-free chat, at paul@nomoss.co, I’d love to hear from you!

--

--

Head of Product at No Moss. Talk to me about building the right thing for the right people at the right time, electric cars, AI, and the best BBH in Melb