jest .each name access object key
Asked Answered
I

8

46

Is it possible to access an object's key inside the name portion of a .each?

let accounts =
    [
        {
            details:
            {
                company_name:
                    "company_name",
                email,
                password:
                    "asdf",
            },
            find:
            [
                "_id",
                "company_name",
                "email",
                "type",
            ],
            type:
                "creator"
        },
        {
            details:
            {
                email,
                first_name:
                    "first_name",
                last_name:
                    "last_name",
                password:
                    "asdf",
            },
            find:
            [
                "_id",
                "email",
                "first_name",
                "last_name",
                "type",
            ],
            type:
                "user"
        },
    ]

describe.each(accounts)(
    "%s", // <-- access the 'type' key, e.g. account.type
    function (account)
    {
        // test code
    }
)
Infinite answered 28/6, 2019 at 1:49 Comment(5)
What do you mean, "object's key"? An object contains keys. Do you mean the index?Lugubrious
@JackBashford trying to access the type in the objectInfinite
Oh. So in the first iteration, it'd be creator, second iteration, user. Right?Lugubrious
@JackBashford yesInfinite
since describe.each utilizes util.format for generating the name I don't see a way achieving the goal. util.format does not provide a way to access particular propertyXylotomy
P
39

Jest describe.each expects an array of arrays in the first parameter. If you pass in a 1D array, internally it will be mapped to an array of arrays (i.e. passing [1, 2, 3] as first parameter would be converted to [[1], [2], [3]]).

Each one of the arrays inside of the array is used as the data for a test suite. So, in the previous example, describe.each would generate three test suites, the first with 1 as data, the second with 2 as data and the third with 3 as data.

Now, in the test suite name, you can only format the parameters you are providing to it. In your case, you are passing to each test suite the data in each object of the accounts array. So, when you set the format specifiers in the test suite name, they will apply to the whole account object (i.e. the %s in your example will stringify your object resulting in [object Object]). Unfortunately, I don't think you can apply the format specifiers to a key of the object.

Some ideas to accomplish what you want:

Solution 1

If you use the %s formatter to compose the test suite name, the toString method of Object will be called (which by default returns [object Object]).

If you define a toString method in each of your accounts objects, that method will be used instead. So, we could add the toString method to each one of the account objects with this code (note that the toString method we are adding is returning the value for the type key):

const accounts = [{
    details: {
        company_name: "company_name",
        email: "aa",
        password: "asdf",
    },
    find: [ "_id", "company_name", "email", "type", ],
    type: "creator"
}, {
    details: {
        email: 'bb',
        first_name: "first_name",
        last_name: "last_name",
        password: "asdf",
    },
    find: [ "_id", "email", "first_name", "last_name", "type", ],
    type: "user"
}].map(account => Object.assign(account, { toString: function() { return this.type; } }));

Now, with the %s format specifier you should see the account type in each test suite:

describe.each(accounts)(
    "%s", // <-- This will cause the toString method to be called.
    function (account)
    {
        // test code
    }
)

Solution 2

You can always redefine each one of your test suite data so that the first parameter is the account type (note that now accounts is a 2D array):

let accounts = [
    [
        "creator",
        {
            details: {
                company_name: "company_name",
                email: "email",
                password: "asdf",
            },
            find: [ "_id", "company_name", "email", "type", ],
            type: "creator"
        }
    ], [
        "user", 
        {
            details: {
                email: "email",
                first_name: "first_name",
                last_name: "last_name",
                password: "asdf",
            },
            find: [ "_id", "email", "first_name", "last_name", "type", ],
            type: "user"
        },
    ]
]

You can now use that first parameter (which is the account type) to give the test suite its name:

describe.each(accounts)(
    '%s',  // <-- This %s will format the first item in each test suite array.
    function (accountType, account) {
        // test code
    }
); 

Note that now your test function receives two parameters as each test suite array has two elements. The first one is the account type and the second one is the account data.

Solution 3

You can use the tagged template literal form of describe.each. With this solution you don't have to change your current definition of accounts array.

describe.each`
    account
    ${accounts[0]}
    ${accounts[1]}
`('$account.type', function (account) { 
    // test code
});

The downside of this solution is that you have to manually append each test suite data in the template literal in a new line (i.e. if you add a new element to the accounts array you have to remember to add it in the template literal in a new line as ${accounts[2]}).

Perigon answered 28/6, 2019 at 23:9 Comment(1)
I like solution 2 the best. Just implemented it and it works perfectlyLigament
P
33

As modern doc says, you can

generate unique test titles by injecting properties of test case object with $variable

So simply:

describe.each(accounts)(
    "$type",
    function (account) {
        // tests
    }
)

You can access nested object values like this: $variable.path.to.value

The same works on test.each level.

Pepsin answered 7/11, 2021 at 13:32 Comment(3)
Make sure you have the correct version installed for this. This feature is only supported for 27.0+Verine
it is not working for me with version "jest": "^27.1.0", example test.each(cases)('$variable', (a: number, b: number, expected: boolean) => { expect(isInGroup(a, 3, b, 3)).toBe(expected); });Smallish
Same with me for version 28.1.2, it just prints out '$type.name' instead of the object's nameNerti
H
23

you can map your initial account array to convert each account into an array with 2 items:

  1. the account type
  2. the initial account element

Now, you can use the first element array in describe name

describe.each(accounts.map(account => [account.type, account]))(
    'testing %s', // %s replaced by account type
    (type, account) => { // note: 2 arguments now
        it('details should be defined ', () => {
            expect(account.details).toBeDefined();
        });
    },
);

Hinshelwood answered 29/6, 2019 at 14:8 Comment(2)
if you use snapshot matching you need to create a unique title for each test as this will be the name for the snapshot file.Baroque
I like this as the cleanest answer!Boulevard
L
5

I had a similar problem with an object. I wanted to test an error message depending on http error codes, so I wrote a test object like so:

const expectedElements = {
  error: {
    code: 500,
    title: "Problème avec l'API"
  },
  notFound:{
    code: 404,
    title: "Élement absent"
  },
  unauthorized:{
    code: 401,
    title: "Accès non autorisé"
  }
};

I used Object.entries(obj) to get an array with those entries written like so: ['key','value']. I can access thoses as two parameters in the test. Here's how I wrote it:

test.each(Object.entries(expectedElements))("NoAccess show the right element for %s",(key,expectedElement)=>{
    const { getByRole } = render(<NoAccess apiStatusCode={expectedElement.code}/>);
    //test code
  });

Now I can add cases as much as I want and I won't have to rewrite the test or create an array. I just write an new value in my expectedElements object. Bonus, I also have a descriptive test name!

Lutetium answered 20/8, 2020 at 14:47 Comment(0)
D
3

Modern Solution

No need for any of the remapping or toString madness.

These days, test.each supports key access when using a list of objects. For your use case, given the list of accounts, you would simply:

describe.each(accounts)('$type', function (account) {
   // test code
})
Dubois answered 28/7, 2023 at 18:8 Comment(0)
F
1

Another alternative is to create a wrapper class and stick to a simple convention:

class TestCase {
  constructor(value) {
   this._value = value;
  }

  get value() {
   return this._value;
  }

  toString() {
    return JSON.stringify(this._value);
  }
}

Then a test will look like this:

const testCases = accounts.map(TestCase)
describe.each(accounts)(
  "%s", // <-- you can customize this in TestCase toString
    function ({value: account})
    {
        // test code 
    }
)
Form answered 9/2, 2022 at 14:41 Comment(0)
H
1

For those, who cannot use astef's solution of just using $variable.path.to.value in test.each block (without describe block)

Make sure you pass dictionary.

So simple array myArray = [1,2,3] will become myArray = [{number:1},{number:1}], and then you should:

test.each(myArray)("My lucky $number", async ({ number } ) => {...

In place of an actual number you could put object, and then access it's properties

Hullo answered 1/9, 2023 at 15:23 Comment(0)
B
0

Slightly unrelated since this is vitest but you can use $ to represent the object itself, with the desired field's name directly following

it.each(config.chains)('should have mainnet rpc urls for production - $name', ...

will print out

enter image description here

Bayer answered 4/6, 2024 at 21:11 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.