Pragmatic Testing in AdonisJS with Japa #2.4

Data-Driven Tests with Datasets

In This Lesson

Learn to write DRY tests using Japa's datasets! Define an array or callback function of data (primitives or objects) to run a single test multiple times, enabling TypeScript inference, and using interpolation in the test name for context.

Created by
@tomgobich
Published

Notes Used to Craft this Lesson

DRY stands for Don't Repeat Yourself and in testing, it is easy to accidentally repeat yourself if you need to test the same thing using different inputs or outputs.

That's where the magic of datasets in Japa comes in to help. We can easily use them to define an array of data to run a single test with, so that we don't have to copy/paste that test over and over again, keeping them nice and DRY.

Like the others, let's make a new spec for datasets here:

node ace make:test datasets --suite=unit
Copied!

Within our TestService we have a method that tests to see if a provided email is valid. Later on, we'll learn how we can actually use VineJS (AdonisJS' validator) for this, but for now, let's keep it simple.

We'll also be using our TestService in each of these methods, and this service doesn't hold any state, so I'm just going to instantiate it at the top of our group here so it's available in each test.

test.group("Datasets", () => {
  const testService = new TestService();

  test("validate string list of emails", async ({ assert }, email) => {
    const result = testService.isValidEmail(email);
    assert.isTrue(result);
  }).with(["johndoe@test.com", "jane.doe@test.com", "bob+dylan@test.com"]);
});
Copied!

Now, Japa will ultimately provide our dataset, where we previously saw it provide the done method. It knows to do this because we've called the with method, however, TypeScript isn't happy with it. But as we can see in our output, everything works perfectly fine.

To allow TypeScript to infer the type here correctly, we can change how we're calling our test.

test.group("Datasets", () => {
  const testService = new TestService();

  test("validate string list of emails")
    .with(["johndoe@test.com", "jane.doe@test.com", "bob+dylan@test.com"])
    .run(async ({ assert }, email) => {
      const result = testService.isValidEmail(email);
      assert.isTrue(result);
    });
});
Copied!

Rather than actually performing our test in a callback within the test method, we can instead move it to a callback within the run method. This will behave exactly the same as we had it before. Then, we can move the with method before the run method to allow TypeScript to correctly infer the run method's argument types. With that, now everything is happy.

Now, within our test runner output, we see the same test name printed 3 times, once for each email in our dataset. We can provide ourselves with some context as to which email is being tested by using interpolation within our test's name. When we provide just an array of primitives, as we have here, this will be provided as $self

test.group("Datasets", () => {
  const testService = new TestService();

  test("validate string list of emails (testing: {$self})")
    .with(["johndoe@test.com", "jane.doe@test.com", "bob+dylan@test.com"])
    .run(async ({ assert }, email) => {
      const result = testService.isValidEmail(email);
      assert.isTrue(result);
    });
});
Copied!

We can also provide an array of objects, and within the object, we can specify expected assertion outcomes! To aid with this, I have a list of objects within our TestService that contains an email and the expected assertion result.

test("validate email dataset")
  .with(testService.getEmailDataset())
  .run(async ({ assert }, { email, result }) => {
    const isValid = testService.isValidEmail(email);
    assert.equal(isValid, result);
  });
Copied!

Then, again, if we want to specify which email is being tested, we can do so using interpolation; however, this time, we can directly use the property name within the object!

++test("validate email dataset (testing: {email})")
  .with(testService.getEmailDataset())
  .run(async ({ assert }, { email, result }) => {
    const isValid = testService.isValidEmail(email);
    assert.equal(isValid, result);
  });
Copied!

Finally, with can be asynchronous! So if you need to fetch a list from somewhere, that can be easily done, and to mimic this, I have an async version of our getEmailDataset to simulate this.

test("validate email using async dataset (testing: {email})")
  .with(async () => testService.getEmailDatasetAsync())
  .run(async ({ assert }, { email, result }) => {
    const isValid = testService.isValidEmail(email);
    assert.equal(isValid, result);
  });
Copied!

Join the Discussion 0 comments

Create a free account to join in on the discussion
robot comment bubble

Be the first to comment!