A Gentle Introduction to Test-Driven Development: Creating an Object Validator
Tags: javascript, node, webdev, programming
Published: 2020-07-27 12:49:18

---
title: "A Gentle Introduction to Test-Driven Development: Creating an Object Validator"
published: true
description: Test-Driven Development (TDD) seems like a great concept, but it's hard to fully understand and appreciate until you see it in action. In this blog post, we're going to implement a JavaScript object validator using TDD.
tags: javascript, node, webdev, programming
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/i3oto3exzll441g08a9g.png
canonical_url: https://typeofnan.dev/creating-an-object-validator-the-tdd-way/
---

Test-Driven Development (TDD) seems like a great concept, but it's hard to fully understand and appreciate until you see it in action. In this blog post, we're going to implement a JavaScript object validator using TDD.

*Please give this post a 💓, 🦄, or 🔖 if it you learned something!*

<hr />

I make other easy-to-digest tutorial content! Please consider:

- Subscribing to my [DevTuts mailing list](https://buttondown.email/devtuts)
- Subscribing to my [DevTuts YouTube channel](https://youtube.com/c/devtutsco)

<hr />

# A Quick Primer on Test-Driven Development

TDD flips a lot of "conventional" software development processes upside-down by writing tests first and then writing code that will satisfy those tests. Once the tests are passing, the code is refactored to make sure it's readible, uses consistent style with the rest of the codebase, is efficient, etc. My perferred way to remember this process is **Red, Green, Refactor**:

## Red :x: -> Green :heavy_check_mark: -> Refactor :recycle:

1. **Red :x:** - Write a test. Run your tests. The new test fails since you haven't written any code to pass the test yet.
2. **Green :heavy_check_mark:** - Write code that passes your test (and all previous tests). Don't be clever, just write code so your tests pass!
3. **Refactor :recycle:** - Refactor your code! There are many reasons to refactor, such as efficiency, code style, and readibility. Make sure your code still passes your tests as you refactor.

The beauty in this process is that, as long as your tests are representative of your code's use cases, you'll now be developing code that (a) doesn't include any gold-plating and (b) will be tested each time you run tests in the future.

# Our TDD Candidate: An Object Validator

Our TDD candidate is an object validation function. This is a function that will take an object and some criteria as inputs. Initially, our requirements will be as follows:

- The validator will take two arguments: an object to be validated and an object of criteria
- The validator will return an object with a boolean `valid` property that indicates if the object is valid (`true`) or invalid (`false`).

Later, we will add some more complex criteria.

# Setting Up Our Environment

For this exercise, let's create a new directory and install `jest`, which is the test framework we'll using.

```bash
mkdir object-validator
cd object-validator
yarn add jest@24.9.0
```

**Note:** The reason you're installing jest specifically at version 24.9.0 is to make sure your version matches the version I'm using in this tutorial.

The last command will have created a `package.json` file for us. In that file, let's change the scripts section to enable us to run jest with the `--watchAll` flag when we run `yarn test`. This means all test will re-run when we make changes to our files!

Our `package.json` file should now look like this:

```json
{
  "scripts": {
    "test": "jest"
  },
  "dependencies": {
    "jest": "24.9.0"
  }
}
```

Next, create two files: `validator.js` and `validator.test.js`. The former will contain the code for our validator and the latter will contain our tests. (By default, jest will search for tests in files that end with `.test.js`).

# Creating an Empty Validator and Initial Test

In our `validator.js` file, let's start by simply exporting `null` so we have something to import into our test file.

#### validator.js

```javascript
module.exports = null;
```

#### validator.test.js

```javascript
const validator = require('./validator');
```

## An Initial Test

In our initial test, we'll check that our validator considers an object valid if there are no criteria provided. Let's write that test now.

#### validator.test.js

```javascript
const validator = require('./validator');

describe('validator', () => {
  it('should return true for an object with no criteria', () => {
    const obj = { username: 'sam21' };
    expect(validator(obj, null).valid).toBe(true);
  });
});
```

Now we run the test! Note we haven't actually written any code for our `validator` function, so this test _better_ fail.

> **Do not skip this step!** It's always tempting to skip the "red" part of the red-green-refactor cycle, but you should always take the time to fail your tests first. This is so you can test your test... in other words, you need to confirm your test fails when it should, otherwise it's not testing your software correctly.

```bash
yarn test
```

If all is well, you should see that our test failed:

```
validator
  ✕ should return true for an object with no criteria (2ms)
```

## Make the Test Pass

Now that we've confirmed the test fails, let's make it pass. To do this, we'll simple have our `validator.js` file export a function that returns the desired object.

#### validator.js

```javascript
const validator = () => {
  return { valid: true };
};

module.exports = validator;
```

Our tests should still be running in the console, so if we take a peek there we should see our test is now passing!

```
validator
  ✓ should return true for an object with no criteria
```

# Continue the Cycle...

Let's add a couple more tests. We know that we want to either pass or fail an object based on criteria. We will now add two tests to do this.

#### validator.test.js

```javascript
it('should pass an object that meets a criteria', () => {
  const obj = { username: 'sam123' };
  const criteria = obj => obj.username.length >= 6
  };
  expect(validator(obj, criteria).valid).toBe(true);
});
it('should fail an object that meets a criteria', () => {
  const obj = { username: 'sam12' };
  const criteria = obj => obj.username.length >= 6,
  };
  expect(validator(obj, criteria).valid).toBe(false);
});
```

Now we run our tests to make sure the two new ones fail... but one of them doesn't! This is actually [fairly normal in TDD](https://softwareengineering.stackexchange.com/questions/357099/how-to-deal-with-test-passing-from-the-start-in-tdd) and can often occur because of generalized solutions coincidentally matching more specific requirements. To combat this, I recommend temporarily changing the returned object in `validator.js` to verify the already-passing test can indeed fail. For example, we can show every test fails if we return `{ valid: null }` from our validator function.

```
validator
  ✕ should return true for an object with no criteria (4ms)
  ✕ should pass an object that meets a criteria (1ms)
  ✕ should fail an object that meets a criteria
```

Now, let's pass these tests. We will update our validator function to return the result of passing `obj` to `criteria`.

#### validator.js

```javascript
const validator = (obj, criteria) => {
  if (!criteria) {
    return { valid: true };
  }
  return { valid: criteria(obj) };
};

module.exports = validator;
```

Our tests all pass! We should consider refactoring at this point, but at this point I don't see much opportunity. Let's continue on creating tests. Now, we'll account for the fact that we'll need to be able to evaluate multiple criteria.

```javascript
it('should return true if all criteria pass', () => {
  const obj = {
    username: 'sam123',
    password: '12345',
    confirmPassword: '12345',
  };
  const criteria = [
    obj => obj.username.length >= 6,
    obj => obj.password === obj.confirmPassword,
  ];
  expect(validator(obj, criteria).valid).toBe(true);
});
it('should return false if only some criteria pass', () => {
  const obj = {
    username: 'sam123',
    password: '12345',
    confirmPassword: '1234',
  };
  const criteria = [
    obj => obj.username.length >= 6,
    obj => obj.password === obj.confirmPassword,
  ];
  expect(validator(obj, criteria).valid).toBe(false);
});
```

Our two new tests fail since our `validator` function doesn't expect `criteria` to be an array. We could handle this a couple ways: we _could_ let users either provide a function or an array of functions as criteria and then handle each case within our `validator` function. That being said, I would rather our `validator` function have a consistent interface. Therefore, we will just treat criteria as an array and fix any previous tests as necessary.

Here's our first attempt at making our tests pass:

#### validator.js

```javascript
const validator = (obj, criteria) => {
  if (!criteria) {
    return { valid: true };
  }
  for (let i = 0; i < criteria.length; i++) {
    if (!criteria[i](obj)) {
      return { valid: false };
    }
  }
  return { valid: true };
};

module.exports = validator;
```

Our new tests pass, but now our old tests that treated `criteria` as a function fail. Let's go ahead and update those tests to make sure `criteria` is an array.

#### validator.test.js (fixed tests)

```javascript
it('should pass an object that meets a criteria', () => {
  const obj = { username: 'sam123' };
  const criteria = [obj => obj.username.length >= 6];
  expect(validator(obj, criteria).valid).toBe(true);
});
it('should fail an object that meets a criteria', () => {
  const obj = { username: 'sam12' };
  const criteria = [obj => obj.username.length >= 6];
  expect(validator(obj, criteria).valid).toBe(false);
});
```

All our tests pass, back to green! This time, I think we can reasonably refactor our code. We recall we can use the `every` array method, which is in line with our team's style.

#### validator.js

```javascript
const validator = (obj, criteria) => {
  if (!criteria) {
    return { valid: true };
  }
  const valid = criteria.every(criterion => criterion(obj));
  return { valid };
};

module.exports = validator;
```

Much cleaner, and our tests still pass. Note how confident we can be in our refactor due to our thorough testing!

# Handling a Relatively Large Requirement Change

We're happy with how our validator is shaping up, but user testing is showing that we really need to be able to support error messages based on our validations. Furthermore, we need to aggregate the error messages by field name so we can display them to the user next to the correct input field.

We decide that our output object will need to resemble the following shape:

```javascript
{
  valid: false,
  errors: {
    username: ["Username must be at least 6 characters"],
    password: [
      "Password must be at least 6 characters",
      "Password must match password confirmation"
    ]
  }
}
```

Let's write some tests to accommodate the new functionality. We realize pretty quickly that `criteria` will need to be an array of objects rather than an array of functions.

#### validator.test.js

```javascript
it("should contain a failed test's error message", () => {
  const obj = { username: 'sam12' };
  const criteria = [
    {
      field: 'username',
      test: obj => obj.username.length >= 6,
      message: 'Username must be at least 6 characters',
    },
  ];
  expect(validator(obj, criteria)).toEqual({
    valid: false,
    errors: {
      username: ['Username must be at least 6 characters'],
    },
  });
});
```

We now run our tests and find this last test fails. Let's make it pass.

#### validator.test.js

```javascript
const validator = (obj, criteria) => {
  if (!criteria) {
    return { valid: true };
  }
  const errors = {};
  for (let i = 0; i < criteria.length; i++) {
    if (!criteria[i].test(obj)) {
      if (!Array.isArray(errors[criteria[i].field])) {
        errors[criteria[i].field] = [];
      }
      errors[criteria[i].field].push(criteria[i].message);
    }
  }

  return {
    valid: Object.keys(errors).length === 0,
    errors,
  };
};

module.exports = validator;
```

Now, the first test and the last test pass, but the others are failing. This is because we changed the shape of our `criteria` input.

```
validator
  ✓ should return true for an object with no criteria (2ms)
  ✕ should pass an object that meets a criteria (3ms)
  ✕ should fail an object that meets a criteria
  ✕ should return true if all criteria pass
  ✕ should return false if only some criteria pass
  ✓ should contain a failed test's error message
```

Since we know the `criteria` implementation in the final test case is correct, let's update the middle four cases to pass. While we're at it, let's create variables for our criteria objects to reuse them.

#### validator.test.js

```javascript
const validator = require('./validator');

const usernameLength = {
  field: 'username',
  test: obj => obj.username.length >= 6,
  message: 'Username must be at least 6 characters',
};

const passwordMatch = {
  field: 'password',
  test: obj => obj.password === obj.confirmPassword,
  message: 'Passwords must match',
};

describe('validator', () => {
  it('should return true for an object with no criteria', () => {
    const obj = { username: 'sam21' };
    expect(validator(obj, null).valid).toBe(true);
  });
  it('should pass an object that meets a criteria', () => {
    const obj = { username: 'sam123' };
    const criteria = [usernameLength];
    expect(validator(obj, criteria).valid).toBe(true);
  });
  it('should fail an object that meets a criteria', () => {
    const obj = { username: 'sam12' };
    const criteria = [usernameLength];
    expect(validator(obj, criteria).valid).toBe(false);
  });
  it('should return true if all criteria pass', () => {
    const obj = {
      username: 'sam123',
      password: '12345',
      confirmPassword: '12345',
    };
    const criteria = [usernameLength, passwordMatch];
    expect(validator(obj, criteria).valid).toBe(true);
  });
  it('should return false if only some criteria pass', () => {
    const obj = {
      username: 'sam123',
      password: '12345',
      confirmPassword: '1234',
    };
    const criteria = [usernameLength, passwordMatch];
    expect(validator(obj, criteria).valid).toBe(false);
  });
  it("should contain a failed test's error message", () => {
    const obj = { username: 'sam12' };
    const criteria = [usernameLength];
    expect(validator(obj, criteria)).toEqual({
      valid: false,
      errors: {
        username: ['Username must be at least 6 characters'],
      },
    });
  });
});
```

And if we check our tests, they're all passing!

```
validator
  ✓ should return true for an object with no criteria
  ✓ should pass an object that meets a criteria (1ms)
  ✓ should fail an object that meets a criteria
  ✓ should return true if all criteria pass
  ✓ should return false if only some criteria pass (1ms)
  ✓ should contain a failed test's error message
```

Looks good. Now let's consider how we can refactor. I'm certainly no fan of the nested `if` statement in our solution, and we're back to using `for` loops when our code still tends towards array methods. Here's a better version for us:

```javascript
const validator = (obj, criteria) => {
  const cleanCriteria = criteria || [];

  const errors = cleanCriteria.reduce((messages, criterion) => {
    const { field, test, message } = criterion;
    if (!test(obj)) {
      messages[field]
        ? messages[field].push(message)
        : (messages[field] = [message]);
    }
    return messages;
  }, {});

  return {
    valid: Object.keys(errors).length === 0,
    errors,
  };
};

module.exports = validator;
```

Our tests our still passing and we're pretty happy with how our refactored `validator` code looks! Of course, we can and should keep building out our test cases to make sure we can handle multiple fields and multiple errors per field, but I'll leave you to continue this exploration on your own!

# Conclusion

Test-Driven Development gives us the ability to define the functionality our code needs to have prior to actually writing the code. It allows us to methodically test and write code and gives us a _ton_ of confidence in our refactors. Like any methodology, TDD isn't perfect. It's prone to error if you fail to make sure your tests fail first. Additionally, it can give a false sense of confidence if you're not thorough and rigorous with the tests you write.

<hr />

*Please give this post a 💓, 🦄, or 🔖 if it you learned something!*