Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
September 5, 2022 12:59 am GMT

Make Testing Easier with Test Fixture Generators

When building an application, you need to making testing it as easy as possible. Using a test fixture generator can help keep your tests maintainable and easy to modify as your application changes.

Test data anti patterns

When you first starting building an application, it's easy to fall into some anti-patterns that can lead to problems as your application grows and matures

Static JSON files

The simplest approach that most people use when creating test data is to use a json file. But this approach can run into problems when you need to test different scenarios.
Lets say you have a user object that you've put into a json file
test-user.json

{    "firstName": 'John',    "lastName": "Doe",    "isActive": true}

And import it into your test. But this is now a single instance of the object, so any changes you make will be reflected in all tests in the file. It can lead do

import user from './test-user.json'describe('tests with user', () => {    it('should do something when the user is disabled', () => {        user.isActive = false        //...    })    it('should do something when user is enabled', () => {        //user is disabled in this test because it was changed in the previous test.    })})

This can lead unexpected test failures that can be spot. The more complex the variations, the harder it is to setup your tests correctly. You might be tempted to create different test objects that set to specific states the object could be in for different tests. But that creates another set of problems, because if the object shape changes, you'll need to change multiple states.

When to use

Okay, it's a bit harsh to call this an anti-pattern. Using static JSON files works best when it's static data that doesn't really change or changes to it does not affect the behavior of your application. I typically have a combination of JSON fixtures along side generated fixtures and use them in tandem.

Partial Objects

Another common approach is to include a partial object with each test that have the fields of interest for the test.

it('should do something when the user is disabled', () => {        const user = {isActive: false}        //...    })    it('should do something when user is enabled', () => {        const user = {isActive: true}        // ...            })

This can create a lot of duplication throughout your test code, and if the behavior of your system changes where you need to change the shape or use other fields for making decisions, you've now got a bunch of test data scattered throughout your tests that will need to change.

When to use

Never. Honestly, don't do this. Either use a static JSON file where you'll have a valid object and kept in one place in your code, so you will only need to make changes in one place when that is needed.

Test fixture Generators

Using a test fixture generator solves all of these problems. The test fixture is defined only once, so if there are changes to your object graph, you'll make those changes only once and all tests will be updated.

Test fixtures are created each time you need them, so if you change your fixture, it will not effect other tests (unless you want them too). Those weird test failures will go away and your test harness will be more reliable. As a result, your tests will be easier to manage as your application grows. It also puts developers into the pit of success when creating tests; lowering the barrier to writing more and better tests for your app.

Efate, a modern test fixture generator

Efate is a modern test fixture generator, design for use in JavaScript or Typescript. generates modular fixtures that can be imported in and reduces the use of strings for both fixture definition and usages. It allows you to override specific fields during fixture creation and returning full object definitions with dummy data for other fields. It is also extensible, allowing you to create custom field generator definitions.

Defining a fixture

Let's assume you have a typscript interface for your User object

export interface Account {  userName: string;  passWord: string;}export interface User {    interface User {    id?: number;    email: string;    dateStarted: Date;    roles: string[];    isActive: boolean;    account: Account}

To define a fixture, you need to import a fixture generator factory. The factory is used to allow for extensibility, which we'll talk about in a later post. The factory will return a function used to create the fixtures.

Then you will create the fixture, specifying how each field should be populated.

import {createFixtureFactory} from 'efate';const createFixture = createFixtureFactory();const accountFixture = createFixture<Account>(t => {    t.userName.asString();    t.password.asString();})const userFixture = createFixture<User>(t => {    t.id.asNumber();    t.firstName.asString();    t.email.asEmail();    t.dateStarted.asDate({ incrementDay: true }));    t.roles.asArray();    t.isActive.asBoolean();    t.account.fromFixture(accountFixture);})export {accountFixture, userFixture};

As you can see, you have control over how each field in your object will be created. Some of the definition functions, like asDate takes additional options to control how they are generated. You can also nest your fixtures together to create a robust object graph and the entire graph will be valid. There are a lot more definition functions to use for different types, or with different behavior to give you the flexibility you need to create your fixtures as need. Or you can pass your own function to have complete control over field definition.

If you're using TypeScript, you'll get full autocomplete on the type parameter in the createFixture callback function, making it really easy to create fixtures.

Using a fixture

To use these fixtures in your tests, you simple import the fixture

import userFixture from './fixtures';define('test group', () => {    it('should use the user object', () => {        // creates a user with all default values;        const user = userFixture.create();         //...    })})

This creates a fully populated user object with all field populated with dummy data.

    {        id: 1,        firstName: 'firstName1',        email: '[email protected]',        dateStarted: // valid date object        roles: ['role1', 'role2', 'role3']        isActive: true,        account: {            userName: 'userName1',            password: 'password1'        }    }

Calling create again in the same module will increment the number on each value so they are different, but follow a simple pattern.

If you need to create an object with specific values, you pass an object with the fields you need to specify. If you're using typescript, the overriding object will be typed so you can get autocomplete help there as well.

const user = userFixture.create({isActive: true, roles:['user']);

It creates an object just like above, but those fields have been overridden

    {        id: 2, // incremented if in the same module as previous fixture,        firstName: 'firstName2',        //...        isActive: true,        roles: ['user']    }

You can also override nested objects as well, and all other fields still automatically populated

    /*    will create a user with account.firstName overridden, but account.password still generated    */    const user = userFixture.create({account: {userName: 'custom user name'});

You can also create arrays of data with different ways of overriding the objects in the array

      // create an array with 5 entries, all generated data      const fixtures = userFixture.createArrayWith(5);      // create an array with 5 entries, all with the isActive field overridden      const fixtures = userFixture.createArrayWith(5, {isActive: true});      // create an array with the first 2 entries overriden      const fixtures = userFixture.createArrayWith(5, [      {isActive: true},      {isActive: false}      ]);      // use a function to override array entries      const fixtures = userFixture.createArrayWith(5, (idx, create) => {          if(idx < 2){              return create({firstName: 'alpha'})          } else {              return create({firstName: 'beta'})          }      })

There's a lot more ways to customize your fixtures, The tests are a good way of seeing all of the features and how they work.

I've built lots applications over the course of my career and thousands of tests. Each time that I don't think I need a test fixture generator, I always end up regretting it. I treat my test code like I do my application code. I want it to be as easy to understand as possible with as little duplication as possible. Using generators like efate have helped me do that. I hope it can do the same for you. If you have any suggestions on features to add or suggestions to make it easier to use, I would love your feedback (or better yet a pull request!).


Original Link: https://dev.to/jcteague/make-testing-easier-with-test-fixture-generators-5485

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To