This article was adapted from a Google Tech on the Toilet (TotT) episode. You can download a printer-friendly version of this TotT episode and post it in your office.

By Bartosz Papis

Test-Driven Development (TDD) is the practice of working in a structured cycle where writing tests comes before writing production code. The process involves three steps, sometimes called the red-green-refactor cycle:

  1. Write a failing test 
  2. Make the test pass by writing just enough production code 
  3. Refactor the production code to meet your quality standards
The three steps in TDD

Research shows TDD has several benefits: it improves test coverage, reduces the number of bugs, increases confidence, and facilitates code reuse. This practice also helps reduce distractions and keep you in the flow. TDD also has its limitations and is not a silver bullet! See the Wikipedia article about TDD for a detailed explanation and references.

Here is a short practical example. Assume you need to modify the following voting algorithm to support the option for voters to abstain:

def outcome(ballots):

  if ballots.count(Vote.FOR) > len(ballots) / 2:

    return "Approved"

  return "Rejected"

1. We start by writing a failing test - as expected, the test doesn't even compile:

def test_abstain_doesnt_count(self):

  self.assertEqual(outcome([Vote.FOR, Vote.FOR, Vote.AGAINST, Vote.ABSTAIN]), "Approved")

2. We fix the compilation error by including the missing enum option:

class Vote(Enum):

  FOR = 1

  AGAINST = 2

  ABSTAIN = 3

Now that the test compiles, we fix the production code to get all tests passing:

def outcome(ballots):

  if ballots.count(Vote.FOR) > (len(ballots) - ballots.count(Vote.ABSTAIN)) / 2:

    return "Approved"

  return "Rejected"

3. We now refactor the code to improve clarity, and complete an iteration of the TDD cycle:

def outcome(ballots):

  counts = collections.Counter(ballots)

  return "Approved" if counts[Vote.FOR] > counts[Vote.AGAINST] else "Rejected"


Learn more about TDD in the book
Test Driven Development: By Example, by Kent Beck.

By Zhe Lu

We all make mistakes. But big mistakes can cause big headaches! Suppose you're writing a utility to update production data for a launch. Before making changes to production data, you want to perform a dry run to validate the expected changes. In your excitement, you forget to include the --dry_run flag in your command:

$ /scripts/credit_accounts --amount=USD10  # Oops, I forgot to include --dry_run

You realize your mistake too late. Safe flag defaults can prevent a simple mistake from turning into a major outage:

Flag has unsafe default:

cliArgs.addBoolFlag(name="dry_run", default=False, help="If set, print change summary, but do NOT change data.")

Flag has safe default:

cliArgs.addBoolFlag(name="dry_run", default=True, help="If set, print change summary, but do NOT change data.")

Safety depends on context: When defining flags, choose the default that minimizes the cost of potential mistakes. This might involve defaulting to a "dry" run, asking for user confirmation before irreversible actions, requiring a confirmation flag on the command line, or other strategies. If you’re writing documentation that contains commands, always set values to minimize the damage if run blindly:

Flag in documentation has unsafe default:

## How to commit changes


Use this command to commit changes. Use --dry_run to test and compute and report changes.


```shell

/scripts/credit_accounts --amount=[value] --filter=[conditions]

```

Flag in documentation has safe default:

## How to commit changes


Use this command to compute and report changes. Use --nodry_run to commit the changes.


```shell

/scripts/credit_accounts --amount=[value] --filter=[conditions]

```

Similarly, consider requiring that environment-specific flags (e.g., backend addresses and output folders) be explicitly set. In this situation, unspecified environment flags will crash your program, instead of potentially mixing configuration across environments.


By Arham Jain

Is your code a tangled mess of business logic and side effects? Mixing database calls, network requests, and other external interactions directly with your core logic can lead to code that’s difficult to test, reuse, and understand. Instead, consider writing a functional core that’s called from an imperativ​​e shell.

Diagram of functional core, imperative shell

Separating your code into functional cores and imperative shells makes it more testable, maintainable, and adaptable. The core logic can be tested in isolation, and the imperati​​ve shell can be swapped out or modified as needed. Here’s some messy example code that mixes logic and side effects to send expiration notification emails to users:

// Bad: Logic and side effects are mixed

function sendUserExpiryEmail(): void {

  for (const user of db.getUsers()) {

    if (user.subscriptionEndDate > Date.now()) continue;

    if (user.isFreeTrial) continue;

    email.send(user.email, "Your account has expired " + user.name + “.”);

  }

}

A functional core should contain pure, testable business logic, which is free of side effects (such as I/O or external state mutation). It operates only on the data it is given.

An imperative shell is responsible for side effects, like database calls and sending emails. It uses the functions in your functional core to perform the business logic.

Rewriting the above code to follow the functional core / imperative shell pattern might look like:

Functional core

function getExpiredUsers(users: User[], cutoff: Date): User[] {

  return users.filter(user => user.subscriptionEndDate <= cutoff && !user.isFreeTrial);

}

function generateExpiryEmails(users: User[]): Array<[string, string]> {

  return users.map(user => 

    ([user.email, “Your account has expired “ + user.name + “.”])

  );

}

Imperative shell

email.bulkSend(generateExpiryEmails(getExpiredUsers(db.getUsers(), Date.now())));

Now that the code is following this pattern, adding a feature to send a new type of email is as simple as writing a new pure function and reusing getExpiredUsers:

// Sending a reminder email to users

function generateReminderEmails(users: User[], cutoff: Date): Array<[string, string]> {...}

const fiveDaysFromNow = ...

email.bulkSend(generateReminderEmails(getExpiredUsers(db.getUsers(), fiveDaysFromNow)));


Learn more in Gary Bernhardt’s original talk about functional core, imperative shell.
No comments


By Kyle Freeman

Imagine you're adding a two-player mode to a game. When testing the feature, you launch the game but don't see the option to add a second player. The configuration looks correct; you enabled two-player mode on the last line!

So what happened? Can you spot the bug in the following example?

allow_warping: false

enable_two_players: false

show_end_credits: true

enable_frost_band: false

enable_two_players: true

Using keep-sorted (github.com/google/keep-sorted) to sort lines makes the error easy to spot: the flag enable_two_players is set twice, with different values:

# keep-sorted start

allow_warping: false

enable_frost_band: false

enable_two_players: false

enable_two_players: true

show_end_credits: true

# keep-sorted end

Sorted lists and lines of code are easier to read and maintain, and can help prevent bugs. To use keep-sorted in your source code, config, and text files, install keep-sorted and then follow these instructions: 

  1. Add keep-sorted start and keep-sorted end comments in your file, surrounding the lines you want to sort.

  2. Run keep-sorted: keep-sorted [file1] [file2] ...

  3. (Optional) Add keep-sorted to your pre-commit so it runs automatically on git commit

You can add options to override default behavior. For example, you can ignore case, sort numerically, order by prefixes, and even sort by regular expressions:

bosses := []int{

  // keep-sorted start by_regex=//.*

  111213, // Aethon Annie

  52816,  // Blazing Benny

  711,    // Daisy Dragon

  1003,   // Kenzie Kraken

  // keep-sorted end

}

Remember: before sorting, ensure the original order isn't intentional. For example, order can be critical when loading dependencies.


Share on Twitter Share on Facebook
No comments