Error expect received toequal expected deep equality

I am learning unit testing with Jest, This is the function I trying to test. import axios from "axios"; export const call = async () => { return axios.get("https://www.example....

I am learning unit testing with Jest,
This is the function I trying to test.

import axios from "axios";

export const call = async () => {

 return axios.get("https://www.example.com").then((resp) =>{
     const response = resp.data
     let userData = {
         title : response.title ? response.title : "",
         url : response.url ? response.url : "",
         date : response.date ? response.date : " ",
         id : response.id ? response.id : "",
         email: response.email ? response.email : ""
     }
     return Promise.resolve(userData)
 })
}

here is the test.js file —

import axios from "axios"
import {call} from './components/call'

jest.mock('axios')

const expectedResult = {
    title:"hello"
}

const resp ={
    data : expectedResult
}

describe("test", ()=>{
    test("demo-test", async ()=>{
        axios.get.mockResolvedValueOnce(resp)
        const response = await call()
        expect(response).toEqual(expectedResult)
        
        console.log("axios.get() returns >>>", axios.get.mock.results[0]);
        expect(axios.get).toHaveBeenCalledWith("https://www.example.com")
    })

})

Here is the error I am getting

expect(received).toEqual(expected) // deep equality

    - Expected  - 0
    + Received  + 4

      Object {
    +   "date": " ",
    +   "email": "",
    +   "id": "",
        "title": "hello",
    +   "url": "",
      }

      16 |         axios.get.mockResolvedValueOnce(resp)
      17 |         const response = await call()
    > 18 |         expect(response).toEqual(expectedResult)
         |                          ^
      19 |
      20 |         console.log("axios.get() returns >>>", axios.get.mock.results[0]);
      21 |         expect(axios.get).toHaveBeenCalledWith("https://google.com")
 FAIL  src/call.test.js (7.597 s)

In the call.js, when userData dosen’t have ternary conditions then the test passes. But when I put ternary condition the test gets failed with the above error. Please help me. How to resolve this issue.

#javascript #reactjs #jestjs #react-testing-library

Вопрос:

Я изучаю модульное тестирование с помощью Jest, это функция, которую я пытаюсь протестировать.

 import axios from "axios";

export const call = async () => {

 return axios.get("https://www.example.com").then((resp) =>{
     const response = resp.data
     let userData = {
         title : response.title ? response.title : "",
         url : response.url ? response.url : "",
         date : response.date ? response.date : " ",
         id : response.id ? response.id : "",
         email: response.email ? response.email : ""
     }
     return Promise.resolve(userData)
 })
}
 

вот это test.js файл —

 import axios from "axios"
import {call} from './components/call'

jest.mock('axios')

const expectedResult = {
    title:"hello"
}

const resp ={
    data : expectedResult
}

describe("test", ()=>{
    test("demo-test", async ()=>{
        axios.get.mockResolvedValueOnce(resp)
        const response = await call()
        expect(response).toEqual(expectedResult)
        
        console.log("axios.get() returns >>>", axios.get.mock.results[0]);
        expect(axios.get).toHaveBeenCalledWith("https://www.example.com")
    })

})
 

Вот ошибка, которую я получаю

 expect(received).toEqual(expected) // deep equality

    - Expected  - 0
      Received    4

      Object {
        "date": " ",
        "email": "",
        "id": "",
        "title": "hello",
        "url": "",
      }

      16 |         axios.get.mockResolvedValueOnce(resp)
      17 |         const response = await call()
    > 18 |         expect(response).toEqual(expectedResult)
         |                          ^
      19 |
      20 |         console.log("axios.get() returns >>>", axios.get.mock.results[0]);
      21 |         expect(axios.get).toHaveBeenCalledWith("https://google.com")
 FAIL  src/call.test.js (7.597 s)
 

В call.js, когда данные пользователя не содержат троичных условий, тест проходит. Но когда я ставлю троичное условие, тест завершается неудачей с вышеуказанной ошибкой. Пожалуйста, помогите мне. Как решить эту проблему.

Комментарии:

1. Можем ли мы предположить, что «когда у userData нет троичных условий», означает, что каждое свойство выглядит примерно так title : response.title ?

2. В ваших expectedResult свойствах date , email , id , url undefined в то время как в call() них есть пустая строка в качестве значения по умолчанию. Так почему же вы ожидаете, что тест пройдет успешно?

3. @Cat да, так и будет title : response.title

4. Какой из них правильно реализован, ваш тест или ваша функция? Если вы ожидаете, что свойства будут инициализированы как undefined , чем ваша реализация теста правильна, и вам нужно изменить свою call() реализацию, чтобы позволить тесту пройти. Если вы ожидаете, что они будут инициализированы как пустые строки, то ваша тестовая реализация неверна, и вам нужно изменить expectedResult объект. Кстати, из вашего кода похоже date , что свойство на самом деле содержит пробел.

5. Другим вариантом может быть то, что в своем тесте вы на самом деле не заботитесь о свойствах, отличных от title (я не знаю, что вы на самом деле пытаетесь здесь протестировать). Если это так, просто измените чек: expect(response.title).toEqual(expectedResult.title)

I am also running into this bug where I have a frozen object, and comparing it against one that I want it to be equal to, using the toEqual matcher. Is it actually fixed?

Skjermbilde 2020-03-13 kl  19 33 37

// createItem.test.js

const createItem = require('./createItem')

describe('createItem', () => {

  it('returns a valid menu item', () => {
    const actual = createItem({
      allergies: ['MU', 'F'],
      category: 'sushi',
      price: 159,
      title: 'my test item'
    })

    expect(actual).toBe({
      title: 'my test item', 
      price: 159, 
      category: 'sushi',
      allergies: [
        {
          code: 'MU',
          nameNO: 'sennep', 
          nameEN: 'mustard'
        }, 
        { 
          code: 'F', 
          nameNO: 'fisk', 
          nameEN: 'fish' 
        }
      ]
    })
  })
})

// createItem.js

const createItem = ({
  title, 
  category, 
  price,
  allergies
} = {}) => {
  if(!title || typeof title !== 'string') {
    throw new Error('Invalid or not existing property')
  }

  if(!allergies || !allergies.length) {
    throw new Error('Invalid or not existing property')
  }

  if(!price || typeof price !== 'number') {
    throw new Error('Invalid or not existing property')
  }

  if(!category || typeof category !== 'string') {
    throw new Error('Invalid or not existing property')
  }

  return Object.freeze({
    title,
    category,
    price,
    allergies
  })
} 

module.exports = createItem

UPDATE: If I rename the allergies-variable I have to something else, it works properly. I do not understand this at all.

Сергей Соловьев

Здравствуйте, Александр. Это буквально означает, что тестировщик получил массив, возвращенный вашей функцией, и сравнил все его внутренние элементы с элементами массива, являющимся правильным решением.

Если говорить про ‘deep equality’ прямо совсем по канону, то «глубокое равенство» — это рекурсивное сравнение всех свойств экземпляров объектов (но, пока не заморачивайтесь с этим).

В этом выводе:

expect(received).toEqual(expected) // deep equality

    - Expected  - 3
    + Received  + 0

      Array [
        1,
    -   3,
    -   2,
    -   9,
      ]

написано буквально следующее:

‘- Expected — 3’ // Ожидал, но не получил 3 элемента массива: 3, 2, 9

‘+ Received + 0’ // Лишних элементов не поступило

Элемент под индексом [0] со значением 1 получен верно.

Подсказки по коду не даю, вам осталось чуть-чуть до самостоятельного решения. Отладочная печать прояснит все происходящее внутри функции.

P.S. Если этот комментарий разрешает заданный вами вопрос, пжл, отметьте его как решение. Если нет, смело уточняйте детали.

Александр Лежнин

Сергей Соловьев, простите, но я не понимаю как печать мне поможет найти ошибки.

Сергей Соловьев

Ну, можно разместить в коде вызовы console.log(…) с используемыми переменными и посмотреть во вкладке Output, какие значения и каких типов они принимали на разных стадиях исполнения. Сразу будет, видно где именно код свернул в обочину.

Александр Лежнин

Сергей Соловьев, никогда этого не делал. Не могу понять в какую строку это вставлять.

Станислав Дзисяк

Приветствую, Александр!

Порекомендую вам пройти урок «Отладочная печать». В нём объясняется как использовать console.log() для отладки кода. На самом деле это очень нужный навык, который вам пригодится и не раз, в том числе и в реальной разработке. Изучайте его и добавляйте в свой арсенал полезных навыков.

Александр Лежнин

Станислав Дзисяк, я подставил console.log. Моё ревью
Не могу понять, к сожалению. Может я и не туда подставляю его. Помогите разобраться, пожалуйста.

Сергей Соловьев

Здравствуйте, Александр. Время — к вечеру, отвечу вместо Станислава, не в обиду ему )

Думаю, с консоль-логом вы хорошо уловили идею. Все именно так. Этот вызов можно размещать буквально по каждой операции в коде и пошагово отслеживать состояние программы — какие значения на каком шаге были присвоены, что возвращают предикаты, какие циклы запустились, а какие нет, или что-то еще типа того. В итоге в голове начинается складываться умозрительная последовательность, что же делает этот код на самом деле.

Теперь надо считать смысл полученных сообщений.

Смотрите, я в ваш вызов консоль-лога для удобства поместил еще ссылку на ‘coll’, чтобы посмотреть структуру объекта, который получил цикл ‘for of’ для обхода и что там произошло:

console.log(coll, flatItems, nonArrayItems); // первым параметром добавился 'coll'
console.log(flat);

Проверяю и во вкладке Output получаю такую отладочную печать (комментарии мои):

console.log
    [ 1, [ 3, 2 ], 9 ] [] [ 1 ] // это возврат из вызова console.log()
                                // [ 1, [ 3, 2 ], 9 ] - это 'coll'
                                // [] - это 'flatItems'
                                // [ 1 ] - это 'nonArrayItems'

      at flatten (arrays.js:19:13) // строка №19 из копии вашего кода (у вас может быть другой номер), в которой размещается вызов console.log, вернувший данные выше

  console.log
    [ 1 ]

      at flatten (arrays.js:20:13) // второй вызов console.log() вернул значение 'flat', равное [ 1 ]

И смотрите, больше console.log не был вызван ни разу. Это и есть первая улика на пути к решению: почему цикл ‘for of’ совершил только одну единственную итерацию, хотя переданный ‘coll’ со значением [ 1, [ 3, 2 ], 9 ] состоит из 3-х элементов? Это произошло потому, что цикл прямо в конце первой итерации получил инструкцию вернуть из функции наружу текущее значение ‘flat’ [1].

Поэтому, чуть выше в Output (помните это):

- Expected  - 3
    + Received  + 0

      Array [
        1,
    -   3,
    -   2,
    -   9,
      ]

тестировщик и сказал, что с первым полученным элементом он согласен, а вот других не получил.

Ход рассуждений с консоль-логом примерно такой )

Александр Лежнин

Сергей Соловьев, спасибо Вам за объяснения. У меня уже истерика. Версия 3.0
К сожалению, мой мозг отказывается понимать, что я делаю не так.

Сергей Соловьев

Добрый вечер, Александр. Вы уже близки к решению и ваш код нужно всего лишь избавить от лишнего.

Из задания следует, что при переносе элементов массива ‘coll’ в результирующий массив, мы должны сохранить видимый глазу порядок чисел, по которому они располагаются внутри ‘coll’. То есть, мы не можем в начало результирующего массива сначала отгрузить распакованные вложенные массивы, а в конец поместить числа, которые в ‘coll’ массивами не являлись. Но в этой строке кода, именно это и происходит:

const flat = [...arrayElements, ...nonArrayElements]; // в начало помещены распакованные
                                                      // вложенные массивы '...arrayElements'

Следовательно, два буферных массива ‘arrayElements» и ‘nonArrayElements’ не разрешают поставленной задачи и являются избыточной конструкцией. Вам следует собрать в цикле результирующий массив в одну константу, соблюдая исходный порядок элементов, не забывая передавать в нее вложенные массивы в распакованном через spread виде.

То, что вы вынесли инициализацию массивов за пределы блока ‘for’, это правильно.

Ну, в целом это все, после этого код должен отработать без ошибок )

Если что, пишите вопросы.

Curious about Advanced Jest Testing Features?

Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library.

Get «The Jest Handbook» (100 pages)

I want this

When testing code with Jest, it can sometimes be useful to fail a test arbitrarily.

This post goes through a few scenarios where that might be useful and how to fail a Jest test explicitly/in a forced manner.

It also presents more idiomatic Jest patterns that could be used interchangeably.

Table of Contents

Fail() a synchronous test that should always throw with Jest

Here is our code under test:

function throwOrNot(shouldThrow = false) {
  if (shouldThrow) {
    throw new Error('shouldThrow was true');
  }
  return 'success';
}

Creating a naive test that only tests the “happy” path

Here is the naive test, which succeeds if the error is thrown.

it('should throw if passed true', () => {
  try {
    throwOrNot(true);
  } catch (error) {
    expect(error).toEqual(new Error('shouldThrow was true'));
  }
});

To run this example, see Running the examples to get set up, then run:

yarn test src/naive-throws-synchronous-passes.test.js

As we can see from the output, the test passes when put into the throw branch of the test under code.

PASS src/naive-throws-synchronous-passes.test.js
  ✓ should throw if passed true (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.22s, estimated 2s
Ran all test suites matching /src/naive-throws-synchronous-passes.test.js/i.

Imagine we modified throwOrNot to stop satisfying this test (it doesn’t throw when passed true), the same test still passes.

function throwOrNot() {
  return 'success';
}

it('should throw if passed true', () => {
  try {
    throwOrNot(true);
  } catch (error) {
    expect(error).toEqual(new Error('shouldThrow was true'));
  }
});

To run this example, see Running the examples to get set up, then run:

yarn test src/naive-throws-synchronous-false-positive.test.js

As per the following test run output, the tests are still passing despite the behaviour not being present any more:

PASS src/naive-throws-synchronous-false-positive.test.js
  ✓ should throw if passed true (1ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.762s
Ran all test suites matching /src/naive-throws-synchronous-false-positive.test.js/i.

What we need to do is to make sure the try block doesn’t continue executing if the throwOrNot function executes without issue.

Force fail() a synchronous Jest test

function throwOrNot() {
  return 'success';
}

it('should throw if passed true', () => {
  try {
    throwOrNot(true);
    throw new Error("didn't throw");
  } catch (error) {
    expect(error).toEqual(new Error('shouldThrow was true'));
  }
});

To run this example, see Running the examples to get set up, then run:

yarn test src/fail-throws-synchronous.test.js

Output of the test run shows that if the code doens’t throw, the test suite will fail, which is desired behaviour:

FAIL src/fail-throws-synchronous.test.js
  ✕ should throw if passed true (9ms)

  ● should throw if passed true

    expect(received).toEqual(expected) // deep equality

    Expected: [Error: shouldThrow was true]
    Received: [Error: didn't throw]

       8 |     throw new Error("didn't throw");
       9 |   } catch (error) {
    > 10 |     expect(error).toEqual(new Error('shouldThrow was true'));
         |                   ^
      11 |   }
      12 | });
      13 |

      at Object.toEqual (src/fail-throws-synchronous.test.js:10:19)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.883s
Ran all test suites matching /src/fail-throws-synchronous.test.js/i.

Idiomatic Jest, fail() alternative: check a function throws using the .toThrow Jest matcher

function throwOrNot() {
  return 'success';
}

it('should throw if passed true', () => {
  expect(throwOrNot.bind(null, true)).toThrow(
    new Error('shouldThrow was true')
  );
});

To run this example, see Running the examples to get set up, then run:

yarn test src/fail-throws-synchronous-to-throw.test.js

As in the previous example, the test fails since the code under test doesn’t throw, but this time we get a Received function did not throw error, which is maybe more descriptive and shows the advantage of using the Jest .toThrow matcher.

FAIL src/fail-throws-synchronous-to-throw.test.js
  ✕ should throw if passed true (5ms)

  ● should throw if passed true

    expect(received).toThrow(expected)

    Expected message: "shouldThrow was true"

    Received function did not throw

      4 |
      5 | it('should throw if passed true', () => {
    > 6 |   expect(throwOrNot.bind(null, true)).toThrow(
        |                                       ^
      7 |     new Error('shouldThrow was true')
      8 |   );
      9 | });

      at Object.toThrow (src/fail-throws-synchronous-to-throw.test.js:6:39)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.795s
Ran all test suites matching /src/fail-throws-synchronous-to-throw.test.js/i.

Fail() an async/await Jest test that should always throw with Jest

Creating a naive test that only tests the “happy” path

We define an async function for which we want to throw under some condition (here if passed true when called).

async function asyncThrowOrNot(shouldThrow = false) {
  if (shouldThrow) {
    throw new Error('shouldThrow was true');
  }
  return 'success';
}

The following test does actually test that the code under test behaves as expected (when it does work as expected).

it('should throw if passed true', async () => {
  try {
    await asyncThrowOrNot(true);
  } catch (error) {
    expect(error).toEqual(new Error('shouldThrow was true'));
  }
});

To run this example, see Running the examples to get set up, then run:

yarn test src/naive-throws-asynchronous-passes.test.js

The output of the test works with a correct implementation:

PASS src/naive-throws-asynchronous-passes.test.js
  ✓ should throw if passed true (4ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.724s, estimated 2s
Ran all test suites matching /src/naive-throws-asynchronous-passes.test.js/i.

Imagine we modified asyncThrowOrNot to stop satisfying this test (it doesn’t throw when passed true), the same test still passes.

async function asyncThrowOrNot() {
  return 'success';
}

it('should throw if passed true', async () => {
  try {
    await asyncThrowOrNot(true);
  } catch (error) {
    expect(error).toEqual(new Error('shouldThrow was true'));
  }
});

To run this example, see Running the examples to get set up, then run:

yarn test src/naive-throws-asynchronous-false-positive.test.js

Tests are still passing, despite the code not doing what it’s supposed to (throwing), this is a false positive:

PASS src/naive-throws-asynchronous-false-positive.test.js
  ✓ should throw if passed true (2ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.341s, estimated 2s
Ran all test suites matching /src/naive-throws-asynchronous-false-positive.test.js/i.

As in the previous section, we need to do is to make sure the try block doesn’t continue executing if the asyncThrowOrNot function executes without issue.

Force fail() an asynchronous Jest test

One way to arbitrarily fail a Jest test is to throw an Error in a branch or line of code that shouldn’t be reached:

async function asyncThrowOrNot() {
  return 'success';
}

it('should throw if passed true', async () => {
  try {
    await asyncThrowOrNot(true);
    throw new Error("didn't throw");
  } catch (error) {
    expect(error).toEqual(new Error('shouldThrow was true'));
  }
});

To run this example, see Running the examples to get set up, then run:

yarn test src/fail-throws-asynchronous.test.js

Output shows the test isn’t passing any more (as is expected) but the error message is a bit cryptic Expected: [Error: shouldThrow was true] Received: [Error: didn't throw].

didn't throw happens to be the message we added after await-ing the function under test (see throw new Error("didn't throw");).

FAIL src/fail-throws-asynchronous.test.js
  ✕ should throw if passed true (7ms)

  ● should throw if passed true

    expect(received).toEqual(expected) // deep equality

    Expected: [Error: shouldThrow was true]
    Received: [Error: didn't throw]

       8 |     throw new Error("didn't throw");
       9 |   } catch (error) {
    > 10 |     expect(error).toEqual(new Error('shouldThrow was true'));
         |                   ^
      11 |   }
      12 | });
      13 |

      at Object.toEqual (src/fail-throws-asynchronous.test.js:10:19)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.203s, estimated 2s
Ran all test suites matching /src/fail-throws-asynchronous.test.js/i.

Idiomatic Jest, fail() alternative: check an async function throws using expect().rejects.toEqual

The more idiomatic way to check an async function throws is to use the await or return an expect(fn(param1)).rejects.toEqual(error).

Note: make sure to await or return the expect() expression, otherwise Jest might not see the error as a failure but an UnHandledPromiseRejection

async function asyncThrowOrNot() {
  return 'success';
}

it('should throw if passed true return expect()', async () => {
  return expect(asyncThrowOrNot(true)).rejects.toEqual(
    new Error('shouldThrow was true')
  );
});

it('should throw if passed true await expect()', async () => {
  await expect(asyncThrowOrNot(true)).rejects.toEqual(
    new Error('shouldThrow was true')
  );
});

To run this example, see Running the examples to get set up, then run:

yarn test src/fail-throws-asynchronous-rejects-to-equal.test.js
FAIL src/fail-throws-asynchronous-rejects-to-equal.test.js
  ✕ should throw if passed true return expect() (5ms)
  ✕ should throw if passed true await expect() (1ms)

  ● should throw if passed true return expect()

    expect(received).rejects.toEqual()

    Received promise resolved instead of rejected
    Resolved to value: "success"

      4 |
      5 | it('should throw if passed true return expect()', async () => {
    > 6 |   return expect(asyncThrowOrNot(true)).rejects.toEqual(
        |          ^
      7 |     new Error('shouldThrow was true')
      8 |   );
      9 | });

      at expect (node_modules/expect/build/index.js:138:15)
      at Object.expect (src/fail-throws-asynchronous-rejects-to-equal.test.js:6:10)

  ● should throw if passed true await expect()

    expect(received).rejects.toEqual()

    Received promise resolved instead of rejected
    Resolved to value: "success"

      10 |
      11 | it('should throw if passed true await expect()', async () => {
    > 12 |   await expect(asyncThrowOrNot(true)).rejects.toEqual(
         |         ^
      13 |     new Error('shouldThrow was true')
      14 |   );
      15 | });

      at expect (node_modules/expect/build/index.js:138:15)
      at Object.expect (src/fail-throws-asynchronous-rejects-to-equal.test.js:12:9)

Test Suites: 1 failed, 1 total
Tests:       2 failed, 2 total
Snapshots:   0 total
Time:        1.646s, estimated 2s
Ran all test suites matching /src/fail-throws-asynchronous-rejects-to-equal.test.js/i.

Fail() a synchronous Jest test that shouldn’t throw

By default a synchronous Jest test that shouldn’t throw will fail if it throws:

it('should not throw', () => {
  throw new Error('it threw');
});

To run this example, see Running the examples to get set up, then run:

yarn test src/synchronous-throw-fail.test.js

The following output shows how the test fails when the test throws.

FAIL src/synchronous-throw-fail.test.js
  ✕ should not throw (2ms)

  ● should not throw

    it threw

      1 | it('should not throw', () => {
    > 2 |   throw new Error('it threw');
        |         ^
      3 | });
      4 |

      at Object.<anonymous> (src/synchronous-throw-fail.test.js:2:9)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.172s, estimated 2s
Ran all test suites matching /src/synchronous-throw-fail.test.js/i.

Fail() an async/await Jest test that shouldn’t throw

By default an asynchronous (async/await) Jest test that shouldn’t throw will fail if it throws/rejects:

it('should not top-level throw', async () => {
  throw new Error('it threw');
});
it('should not throw on Promise rejection', async () => {
  await Promise.reject(new Error('Promise rejection'));
});
it('should not throw on async function throw', async () => {
  const throws = async () => {
    throw new Error('async-function throw');
  };

  await throws();
});

To run this example, see Running the examples to get set up, then run:

yarn test src/asynchronous-throw-fail.test.js

The following output shows how the test fails when the test throws.

Note how throw in an it callback async function, await-ing a Promise rejection and throw in an await-ed async function all fail the test.

FAIL src/asynchronous-throw-fail.test.js
  ✕ should not top-level throw (3ms)
  ✕ should not throw on Promise rejection (1ms)
  ✕ should not throw on async function throw

  ● should not top-level throw

    it threw

      1 | it('should not top-level throw', async () => {
    > 2 |   throw new Error('it threw');
        |         ^
      3 | });
      4 | it('should not throw on Promise rejection', async () => {
      5 |   await Promise.reject(new Error('Promise rejection'));

      at Object.<anonymous> (src/asynchronous-throw-fail.test.js:2:9)

  ● should not throw on Promise rejection

    Promise rejection

      3 | });
      4 | it('should not throw on Promise rejection', async () => {
    > 5 |   await Promise.reject(new Error('Promise rejection'));
        |                        ^
      6 | });
      7 | it('should not throw on async function throw', async () => {
      8 |   const throws = async () => {

      at Object.<anonymous> (src/asynchronous-throw-fail.test.js:5:24)

  ● should not throw on async function throw

    async-function throw

       7 | it('should not throw on async function throw', async () => {
       8 |   const throws = async () => {
    >  9 |     throw new Error('async-function throw');
         |           ^
      10 |   };
      11 |
      12 |   await throws();

      at throws (src/asynchronous-throw-fail.test.js:9:11)
      at Object.throws (src/asynchronous-throw-fail.test.js:12:9)

Test Suites: 1 failed, 1 total
Tests:       3 failed, 3 total
Snapshots:   0 total
Time:        1.304s
Ran all test suites matching /src/asynchronous-throw-fail.test.js/i.

Jest is Promise-aware, so throw, rejection is all the same.

Running the examples

Clone github.com/HugoDF/jest-force-fail.

Run yarn install or npm install (if you’re using npm replace instance of yarn with npm run in commands).

Conclusion

The example show you how to use throw new Error('testingError') to force fail() a Jest (and other test library) test.

This works in synchronous and asynchronous (async/await) Jest tests. In the asynchronous case, it’s because Jest is Promise-aware.

In Jest/JavaScript, a fail functions could be defined as follows (just throws an Error):

function fail() {
  throw new Error('Test was force-failed');
}

The idiomatic way to do this in Jest however is to use expect().toThrow() in the synchronous case:

expect(fn.bind(null, param1, param2)).toThrow(new Error('specify the error'));

And return/await expect().rejects.toEqual() in the asynchronous (async/await) case:

return expect(asyncFn(param1, params)).rejects.toThrow(new Error('specific the error'));
// or 
await expect(asyncFn(param1, params)).rejects.toThrow(new Error('specific the error'));

Further Reading

About async functions and the internals of that, I’ve written a longer post: Async JavaScript: history, patterns and gotchas

unsplash-logoAsa Rodger

Get The Jest Handbook (100 pages)

Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library.

Find out more

or

Join 1000s of developers learning about Enterprise-grade Node.js & JavaScript

Тело цикла, как и тело функции — это место выполнения инструкций. Значит, мы можем использовать внутри него всё изученное ранее, например — условные конструкции.

Представьте себе функцию, которая считает, сколько раз входит буква в предложение. Пример её работы:

countChars('Fear cuts deeper than swords.', 'e'); // 4
// Если вы ничего не нашли, то результат — 0 совпадений
countChars('Sansa', 'y'); // 0

Перед тем как посмотреть её содержимое, попробуйте ответить на вопросы:

  • Является ли эта операция агрегацией?
  • Какой будет проверка на вхождение символа?
const countChars = (str, char) => {
  let i = 0;
  let count = 0;
  while (i < str.length) {
    if (str[i] === char) {
      // Считаем только подходящие символы
      count = count + 1;
    }
    // Счетчик увеличивается в любом случае
    i = i + 1;
  }

  return count;
};

Эта задача является агрегирующей. Несмотря на то, что она считает не все символы, для подсчета самой суммы все равно приходится анализировать каждый символ.

Ключевое отличие этого цикла от рассмотренных в наличии условия внутри тела. Переменная count увеличивается только в том случае, когда текущий рассматриваемый символ совпадает с ожидаемым.

В остальном — это типичная агрегатная функция, которая возвращает количество нужных символов вызываемому коду.

Задание

Функция из теории учитывает регистр букв. То есть A и a с её точки зрения разные символы. Реализуйте вариант этой же функции, так чтобы регистр букв был не важен:

countChars('HexlEt', 'e'); // 2
countChars('HexlEt', 'E'); // 2
Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

  • Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.

В моей среде код работает, а здесь нет 🤨

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Прочитал урок — ничего не понятно 🙄

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.

Полезное

  • if…else

Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics

This is part two of Test-Driven Development with React, Jest, and Enzyme. You can find the first part here.

Last time we began with the project overview, which included a brief explanation of Test-Driven Development (TDD), the application design process, and a high-level synopsis of the application components. From there we continued to the project setup and began writing our (failing) tests, then the code to pass those tests, ultimately finishing with our Calculator snapshot. At this point we have finished the UI for the Calculator and Display components, and have begun work on our Keypad component.

Parts:

  • Part 1: In the first part, we’ll set up the overall project and then dive into developing the UI with Test-Driven Development.
  • Part 2 (this post!): In this part, we’ll finish the UI by adding the number and operator keys before we dive in to adding the basic calculator functionality.

component structure

Let’s get back in to the red, green, refactor cycle by testing the rendering of Keypad for numbers and operators!

  • Keypad Component
    • Test for Rendering of numbers and operators in Keypad
    • Add Keypad CSS
  • Key Component
    • Check for Key in Keypad
    • Key renders keyValue
    • Add Key CSS
    • Add Snapshot Testing for Key
    • Refactor Keypad to use Key for numbers, operators, and submit
    • Add Keypad Snapshot
    • Refactor Calculator State
  • Application Functions
  • Click Event Tests
  • Update Display Tests
  • Set Operator Tests
  • Call Operator Tests
  • Final Thoughts

Keypad Component

Test for Rendering of numbers and operators in Keypad

In the same way that we tested for the rendering of the displayValue prop in the Display component, let’s write rendering tests for both the numbers and operators props in the Keypad component.

In Keypad.spec.js, start with the numbers test:

it('renders the values of numbers', () => {
  wrapper.setProps({numbers: ['0', '1', '2']});
  expect(wrapper.find('.numbers-container').text()).toEqual('012');
});

Then update Keypad.jsx to pass the test by adding a map function to iterate through the numbers array along with a container div element to house our new elements:

...
const Keypad = ({
  callOperator,
  numbers,
  operators,
  setOperator,
  updateDisplay,
}) => {
  const numberKeys = numbers.map(number => <p key={number}>{number}</p>);

  return (
    <div className="keypad-container">
      <div className="numbers-container">
        {numberKeys}
      </div>
    </div>
  );
}
...

The Keypad › should render a <div /> should now break, since there is more than one div.

Update the test in Keypad.spec.js:

it('should render 2 <div />'s', () => {
  expect(wrapper.find('div').length).toEqual(2);
});

All pass! Follow the same pattern for operators, in Keypad.spec.js:

it('renders the values of operators', () => {
  wrapper.setProps({operators: ['+', '-', '*', '/']});
  expect(wrapper.find('.operators-container').text()).toEqual('+-*/');
});

Then update the component in the same way we did for numbers, in Keypad.jsx:

...
const Keypad = ({
  callOperator,
  numbers,
  operators,
  setOperator,
  updateDisplay,
}) => {
  const numberKeys = numbers.map(number => <p key={number}>{number}</p>);

  const operatorKeys = operators.map(operator => <p key={operator}>{operator}</p>);

  return (
    <div className="keypad-container">
      <div className="numbers-container">
        {numberKeys}
      </div>
      <div className="operators-container">
        {operatorKeys}
      </div>
    </div>
  );
}
...

This should now break Keypad › should render 2 <div />'s. Update the test in Keypad.spec.js:

it('should render 3 <div />'s', () => {
  expect(wrapper.find('div').length).toEqual(3);
});

Tests are green!

Add Keypad CSS

Now add the Keypad CSS variables along with the component CSS. Navigate to index.css and make the updates to the :root scope:

/*
app variables
*/

:root {
  /* background colors */
  --calculator-background-color: #696969;
  --display-background-color: #1d1f1f;

  /* font */
  --main-font: 'Orbitron', sans-serif;

  /* font colors */
  --display-text-color: #23e000;

  /* font sizes */
  --display-text-size: 4em;

  /* font weights */
  --display-text-weight: 400;

  /* calculator dimensions */
  --calculator-height: 72%;
  --calculator-width: 36%;

  /* display dimensions */
  --display-height: 24%;
  --display-width: 92%;

  /* keypad dimensions */
  --keypad-height: 72%;
  --keypad-width: 96%;
}

/*
media query for tablet or smaller screen
*/

@media screen and (max-width: 1024px) {
  :root {
    /* font sizes */
    --display-text-size: 6em;

    /* calculator dimensions */
    --calculator-height: 100%;
    --calculator-width: 100%;
  }
}

Add the following to Keypad.css:

.keypad-container {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  height: var(--keypad-height);
  padding: 2%;
  width: var(--keypad-width);
}

.numbers-container {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  height: 80%;
  width: 75%;
}

.operators-container {
  display: flex;
  flex-direction: column;
  height: 80%;
  width: 25%;
}

.submit-container {
  height: 20%;
  width: 100%;
}

About these CSS properties:

  • flex-direction: row; sets the layout of the content in the flex-container to row (this is the default direction of display: flex).
  • flex-wrap: wrap; informs the flex-container to wrap the content in the flex-container if it exceeds the flex-container width.
  • flex-direction: column; sets the layout of the content in the flex-container to column.

Finally, import Keypad.css into Keypad.jsx:

import React from 'react';
import PropTypes from 'prop-types';
import './Keypad.css';
...

Start the app:

The browser should now look like this:

keypad render

Key Component

Check for Key in Keypad

Following the same shallow render test pattern we used with the Calculator, Display, and Keypad components, we’ll now check for the existence of the Key component in Keypad.

Add the following test to Keypad.spec.js:

it('should render an instance of the Key component', () => {
  expect(wrapper.find('Key').length).toEqual(1);
});

You may have noticed that in the previous tests we used containsMatchingElement when checking for child components. Since we’ll be rendering 17 different Key elements, each with a different keyAction, keyType, and keyValue, using containsMatchingElement will not work for this example. Instead we will check for the presence of the element(s) by using the find method, and then checking the length of the resulting array.

Create the test suite file for the Key component in «src/components/Key», and then add the shallow render test for Key in Key.spec.js:

import React from 'react';
import { shallow } from 'enzyme';
import Key from './Key';

describe('Key', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallow(
      <Key
        keyAction={jest.fn()}
        keyType={''}
        keyValue={''}
      />
    );
  });

  it('should render a <div />', () => {
    expect(wrapper.find('div').length).toEqual(1);
  });
});

Add the component to Key.jsx:

import React from 'react';
import PropTypes from 'prop-types';

const Key = ({ keyAction, keyType, keyValue }) => <div className="key-container" />;

Key.propTypes = {
  keyAction: PropTypes.func.isRequired,
  keyType: PropTypes.string.isRequired,
  keyValue: PropTypes.string.isRequired,
}

export default Key;

Keypad › should render an instance of the Key component should still fail.

 PASS  src/components/Display/Display.spec.js
 PASS  src/components/Calculator/Calculator.spec.js
 PASS  src/components/App/App.spec.js
 FAIL  src/components/Keypad/Keypad.spec.js
  ● Keypad › should render an instance of the Key component

    expect(received).toEqual(expected) // deep equality

    Expected: 1
    Received: 0

      33 |
      34 |   it('should render an instance of the Key component', () => {
    > 35 |     expect(wrapper.find('Key').length).toEqual(1);
         |                                        ^
      36 |   });
      37 | });
      38 |

      at Object.<anonymous> (src/components/Keypad/Keypad.spec.js:35:40)

 PASS  src/components/Key/Key.spec.js

Test Suites: 1 failed, 4 passed, 5 total
Tests:       1 failed, 13 passed, 14 total
Snapshots:   3 passed, 3 total
Time:        3.069s
Ran all test suites related to changed files.

Import Key component in Keypad.jsx and update the return statement:

...
import Key from '../Key/Key';
import './Keypad.css';

const Keypad = ({
  callOperator,
  numbers,
  operators,
  setOperator,
  updateDisplay,
}) => {
  ...
  return (
    <div className="keypad-container">
      <div className="numbers-container">
        {numberKeys}
      </div>
      <div className="operators-container">
        {operatorKeys}
      </div>
      <Key
        keyAction={callOperator}
        keyType=""
        keyValue=""
      />
    </div>
  );
}
...

The tests should pass.

Key renders keyValue

Next, add a new test to Key.spec.jsx that checks for the presence of the value of keyValue:

it('should render the value of keyValue', () => {
  wrapper.setProps({ keyValue: 'test' });
  expect(wrapper.text()).toEqual('test');
});

Refactor the Key component in Key.jsx:

const Key = ({ keyAction, keyType, keyValue }) => (
  <div className="key-container">
    <p className="key-value">
      {keyValue}
    </p>
  </div>
);

All pass!

Add Key CSS

This is a good place to update our CSS variables and add the Key CSS. Navigate to index.css and make the following updates:

:root {
  /* background colors */
  --action-key-color: #545454;
  --action-key-color-hover: #2a2a2a;
  --calculator-background-color: #696969;
  --display-background-color: #1d1f1f;
  --number-key-color: #696969;
  --number-key-color-hover: #3f3f3f;
  --submit-key-color: #d18800;
  --submit-key-color-hover: #aa6e00;
  ...
  /* font colors */
  --display-text-color: #23e000;
  --key-text-color: #d3d3d3;

  /* font sizes */
  --display-text-size: 4em;
  --key-text-size: 3em;

  /* font weights */
  --display-text-weight: 400;
  --key-text-weight: 700;
  ...
}
...
@media screen and (max-width: 1024px) {
  :root {
    /* font sizes */
    --display-text-size: 10em;
    --key-text-size: 6em;
    ...
  }
}

The full index.css file should now look like:

/*
app variables
*/

:root {
  /* background colors */
  --action-key-color: #545454;
  --action-key-color-hover: #2a2a2a;
  --calculator-background-color: #696969;
  --display-background-color: #1d1f1f;
  --number-key-color: #696969;
  --number-key-color-hover: #3f3f3f;
  --submit-key-color: #d18800;
  --submit-key-color-hover: #aa6e00;

  /* font */
  --main-font: 'Orbitron', sans-serif;

  /* font colors */
  --display-text-color: #23e000;
  --key-text-color: #d3d3d3;

  /* font sizes */
  --display-text-size: 4em;
  --key-text-size: 3em;

  /* font weights */
  --display-text-weight: 400;
  --key-text-weight: 700;

  /* calculator dimensions */
  --calculator-height: 72%;
  --calculator-width: 36%;

  /* display dimensions */
  --display-height: 24%;
  --display-width: 92%;

  /* keypad dimensions */
  --keypad-height: 72%;
  --keypad-width: 96%;
}

/*
media query for tablet or smaller screen
*/

@media screen and (max-width: 1024px) {
  :root {
    /* font sizes */
    --display-text-size: 10em;
    --key-text-size: 6em;

    /* calculator dimensions */
    --calculator-height: 100%;
    --calculator-width: 100%;
  }
}

/*
app CSS reset
*/

body, div, p {
  margin: 0;
  padding: 0;
}

Then add the component CSS in Key.css:

.key-container {
  align-items: center;
  display: flex;
  height: 25%;
  justify-content: center;
  transition: background-color 0.3s linear;
}

.key-container:hover {
  cursor: pointer;
}

.operator-key {
  background-color: var(--action-key-color);
  width: 100%;
}

.operator-key:hover {
  background-color: var(--action-key-color-hover);
}

.number-key {
  background-color: var(--number-key-color);
  width: calc(100%/3);
}

.number-key:hover {
  background-color: var(--number-key-color-hover);
}

.submit-key {
  background-color: var(--submit-key-color);
  height: 100%;
  width: 100%;
}

.submit-key:hover {
  background-color: var(--submit-key-color-hover);
}

.key-value {
  color: var(--key-text-color);
  font-family: var(--main-font);
  font-size: var(--key-text-size);
  font-weight: var(--key-text-weight);
}

The transition: background-color 0.3s linear; property is used to give our hover effects a smooth animation between the non-hover and the on-hover background colors. The first argument (background-color) defines which property to transition, the second (0.3s) specifies the length of the transition in seconds, and the third (linear) is the style of the transition animation.

Last, import the CSS and make the aforementioned updates in Key.jsx:

import React from 'react';
import PropTypes from 'prop-types';
import './Key.css';

const Key = ({ keyAction, keyType, keyValue }) => (
  <div className={`key-container ${keyType}`}>
    <p className="key-value">
      {keyValue}
    </p>
  </div>
);
...

Add Snapshot Testing for Key

With the Key component UI complete, we can add snapshot testing. At the top of the tests in Key.spec.js, add:

it('should render correctly', () => expect(wrapper).toMatchSnapshot());

Again, this test will immediately pass and it will continue passing until a change has been made to the Key component UI.

Refactor Keypad to use Key for numbers, operators, and submit

Since we want to render a Key component for each index of the numbers and operators arrays as well as the submit key, refactor the Keypad › should render an instance of the Key component test in Keypad.spec.js:

it('should render an instance of the Key component for each index of numbers, operators, and the submit Key', () => {
  const numbers = ['0', '1'];
  const operators = ['+', '-'];
  const submit = 1;
  const keyTotal = numbers.length + operators.length + submit;
  wrapper.setProps({ numbers, operators });
  expect(wrapper.find('Key').length).toEqual(keyTotal);
});

Refactor the map functions and the Key component in the return statement of Keypad.jsx:

...
const Keypad = ({
  callOperator,
  numbers,
  operators,
  setOperator,
  updateDisplay,
}) => {
  const numberKeys = numbers.map(number => (
    <Key
      key={number}
      keyAction={updateDisplay}
      keyType="number-key"
      keyValue={number}
    />)
  );

  const operatorKeys = operators.map(operator => (
    <Key
      key={operator}
      keyAction={setOperator}
      keyType="operator-key"
      keyValue={operator}
    />)
  );

  return (
    <div className="keypad-container">
      <div className="numbers-container">
        {numberKeys}
      </div>
      <div className="operators-container">
        {operatorKeys}
      </div>
      <div className="submit-container">
        <Key
          keyAction={callOperator}
          keyType="submit-key"
          keyValue="="
        />
      </div>
    </div>
  );
}
...

After the refactor, Keypad › should render the Key component for each index of numbers, operators, and the submit Key passes, but the following tests fail:

  1. Keypad › renders the values of numbers
  2. Keypad › renders the values of operators

If you check the test runner, the Keypad › renders the values of operators fail should look like this:

● Keypad › renders the values of operators

  expect(received).toEqual(expected) // deep equality

  Expected: "+-*/"
  Received: "<Key /><Key /><Key /><Key />"

This is due to the shallow rendering method only going one layer deep and returning the contents of the component being shallow rendered and not the actual rendered contents of the child components. In other words, when these tests use find, the return contents are just Key elements, not the actual content inside the Key. For this functionality we can use Enzyme mount, which does a full DOM render and allows to us to get the text values of the child elements. We’ll move these tests into their own describe statement to prevent unnecessary calls to shallow.

As a rule for writing your rendering tests:

  1. Always start with shallow (shallow render)
  2. Use mount, when you want to test either:
    • componentDidMount or componentDidUpdate
    • DOM rendering, component lifecycle, and the behavior of child components

Also, Keypad › should render 3 <div />'s fails because we have added another container div.

Update Keypad.spec.js like so:

import React from 'react';
import { mount, shallow } from 'enzyme';
import Keypad from './Keypad';
import Key from '../Key/Key';

describe('Keypad', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallow(
      <Keypad
        callOperator={jest.fn()}
        numbers={[]}
        operators={[]}
        setOperator={jest.fn()}
        updateDisplay={jest.fn()}
      />
    );
  });

  it('should render 4 <div />'s', () => {
    expect(wrapper.find('div').length).toEqual(4);
  });

  it('should render an instance of the Key component for each index of numbers, operators, and the submit Key', () => {
    const numbers = ['0', '1'];
    const operators = ['+', '-'];
    const submit = 1;
    const keyTotal = numbers.length + operators.length + submit;
    wrapper.setProps({ numbers, operators });
    expect(wrapper.find('Key').length).toEqual(keyTotal);
  });
});

describe('mounted Keypad', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = mount(
      <Keypad
        callOperator={jest.fn()}
        numbers={[]}
        operators={[]}
        setOperator={jest.fn()}
        updateDisplay={jest.fn()}
      />
    );
  });

  it('renders the values of numbers to the DOM', () => {
    wrapper.setProps({ numbers: ['0', '1', '2'] })
    expect(wrapper.find('.numbers-container').text()).toEqual('012');
  });

  it('renders the values of operators to the DOM', () => {
    wrapper.setProps({ operators: ['+', '-', '*', '/'] });
    expect(wrapper.find('.operators-container').text()).toEqual('+-*/');
  });
});

The tests should pass. Run the app. You should see:

keypad render

Add Keypad Snapshot

Now that the UI is completed for the Keypad component, add the snapshot test to Keypad.spec.js:

it('should render correctly', () => expect(wrapper).toMatchSnapshot());

Again, the snapshot test will immediately pass.

Refactor Calculator State

Add the number and operator values to the state object in Calculator.jsx:

...
class Calculator extends Component {
  state = {
    // value to be displayed in <Display />
    displayValue: '0',
    // values to be displayed in number <Keys />
    numbers: ['9', '8', '7', '6', '5', '4', '3', '2', '1', '.', '0','ce'],
    // values to be displayed in operator <Keys />
    operators: ['/', 'x', '-', '+'],
    // operator selected for math operation
    selectedOperator: '',
    // stored value to use for math operation
    storedValue: '',
  }
  ...
}
...

After the changes, the Calculator snapshot breaks since we made changes to the UI of Calculator. We need to update the snapshot. This can be done by entering u in the task runner or by passing the --updateSnapshot flag in when calling the test runner from the command line:

$ npm test --updateSnapshot

Run the app:

final application render

We have completed developing the UI and writing of the render tests for the components. Now we’re ready to move on to giving our calculator functionality.

Application Functions

In this section, we will use TDD to write our application functions, updateDisplay, setOperator, and callOperator by utilizing the redgreenrefactor cycle of creating failing tests and then writing the corresponding code to make them pass. We’ll begin by testing for the click event for the different calculator methods.

Click Event Tests

For each of the calculator methods we’ll write tests that check for calls to the individual methods when the corresponding key type is clicked.

These tests will go in their own describe block as we need to use mount rather than shallow since we are testing the behavior of child components. The tests involve:

  1. Creating a spy using the Jest spyOn method for the calculator method we are testing
  2. Calling forceUpdate to re-render the instance within the test
  3. Using Enzyme’s simulate method on the corresponding Key to create the event

Add the following in Calculator.spec.js:

describe('mounted Calculator', () => {
  let wrapper;

  beforeEach(() => wrapper = mount(<Calculator />));

  it('calls updateDisplay when a number key is clicked', () => {
    const spy = jest.spyOn(wrapper.instance(), 'updateDisplay');
    wrapper.instance().forceUpdate();
    expect(spy).toHaveBeenCalledTimes(0);
    wrapper.find('.number-key').first().simulate('click');
    expect(spy).toHaveBeenCalledTimes(1);
  });

  it('calls setOperator when an operator key is clicked', () => {
    const spy = jest.spyOn(wrapper.instance(), 'setOperator');
    wrapper.instance().forceUpdate();
    expect(spy).toHaveBeenCalledTimes(0);
    wrapper.find('.operator-key').first().simulate('click');
    expect(spy).toHaveBeenCalledTimes(1);
  });

  it('calls callOperator when the submit key is clicked', () => {
    const spy = jest.spyOn(wrapper.instance(), 'callOperator');
    wrapper.instance().forceUpdate();
    expect(spy).toHaveBeenCalledTimes(0);
    wrapper.find('.submit-key').simulate('click');
    expect(spy).toHaveBeenCalledTimes(1);
  });
});

Don’t forget to import mount:

import { mount, shallow } from 'enzyme';

Now refactor Key.jsx to execute the calculator methods on click events:

...
const Key = ({ keyAction, keyType, keyValue }) => (
  <div
    className={`key-container ${keyType}`}
    onClick={() => keyAction(keyValue)}
  >
    <p className="key-value">
      {keyValue}
    </p>
  </div>
);
...

The tests will pass, but the Key snapshot fails. Update the Key snapshot by entering u in the test runner or from the command line run:

$ npm test --updateSnapshot

Now that the onClick handler has been added to Key, run the app and then hop back into the browser and open the JavaScript console. Click on a number key. The output of the click event should look like this:

console log

Now we are ready for the function tests!

Update Display Tests

The updateDisplay method will take a single string argument, value, and update the displayValue in the state object. When displayValue is updated, React will re-render the Display component with the new value of displayValue as the display text.

We need to add a new describe block for updateDisplay in our Calculator test file, and then add our tests for the updateDisplay method. In the tests, updateDisplay will be called from the wrapper.instance() object and the result will be tested against the state object.

Navigate to Calculator.spec.js, declare the describe block, and add the tests inside:

describe('updateDisplay', () => {
  let wrapper;

  beforeEach(() => wrapper = shallow(<Calculator />));

  it('updates displayValue', () => {
    wrapper.instance().updateDisplay('5');
    expect(wrapper.state('displayValue')).toEqual('5');
  });

  it('concatenates displayValue', () => {
    wrapper.instance().updateDisplay('5');
    wrapper.instance().updateDisplay('0');
    expect(wrapper.state('displayValue')).toEqual('50');
  });

  it('removes leading "0" from displayValue', () => {
    wrapper.instance().updateDisplay('0');
    expect(wrapper.state('displayValue')).toEqual('0');
    wrapper.instance().updateDisplay('5');
    expect(wrapper.state('displayValue')).toEqual('5');
  });

  it('prevents multiple leading "0"s from displayValue', () => {
    wrapper.instance().updateDisplay('0');
    wrapper.instance().updateDisplay('0');
    expect(wrapper.state('displayValue')).toEqual('0');
  });

  it('removes last char of displayValue', () => {
    wrapper.instance().updateDisplay('5');
    wrapper.instance().updateDisplay('0');
    wrapper.instance().updateDisplay('ce');
    expect(wrapper.state('displayValue')).toEqual('5');
  });

  it('prevents multiple instances of "." in displayValue', () => {
    wrapper.instance().updateDisplay('.');
    wrapper.instance().updateDisplay('.');
    expect(wrapper.state('displayValue')).toEqual('.');
  });

  it('will set displayValue to "0" if displayValue is equal to an empty string', () => {
    wrapper.instance().updateDisplay('ce');
    expect(wrapper.state('displayValue')).toEqual('0');
  });
});

Now, navigate to Calculator.jsx and update updateDisplay:

...

class Calculator extends Component {
  ...
  updateDisplay = value => {
    let { displayValue } = this.state;

    // prevent multiple occurences of '.'
    if (value === '.' && displayValue.includes('.')) value = '';

    if (value === 'ce') {
      // deletes last char in displayValue
      displayValue = displayValue.substr(0, displayValue.length - 1);
      // set displayValue to '0' if displayValue is empty string
      if (displayValue === '') displayValue = '0';
    } else {
      // replace displayValue with value if displayValue equal to '0'
      // else concatenate displayValue and value
      displayValue === '0' ? displayValue = value : displayValue += value;
    }

    this.setState({ displayValue });
  }
  ...
}
...

You have to be careful about the syntax used in declaring methods in React class-based components. When using es5 object method syntax, the method is not bound to the class by default and binding must be declared explicitly in the constructor method. For example, if you forget to bind this.handleClick and pass it to an onClick handler, this will be undefined when the function is actually called. In this post, we are using the fat arrow method syntax introduced in es6, which handles method binding for us, and allows us to omit the constructor method when initializing our component state.

es5 example:

class Calculator extends Component {
  constructor(props) {
    this.state = {
      displayValue: '0',
    }

    // explicit binding
    this.updateDisplay = this.updateDisplay.bind(this);
  }

  updateDisplay(value) {
    this.setState({ displayValue: value });
  }
}

es6 or later example:

class Calculator extends Component {
  state = {
    displayValue: '0',
  }

  updateDisplay = value => this.setState({ displayValue: value });
}

Refer to the React docs for more info on binding.

All tests should now pass, navigate to the browser and click the number keys to see the display update.

number keys

Now move on to the setOperator method!

Set Operator Tests

The setOperator method will take a single string argument, value, and it will update displayValue, selectedOperator, and storedValue in the state object.

Again, add a describe block for setOperator in our Calculator test file and then add the tests for the setOperator method. Like before, setOperator will be called from the wrapper.instance() object and the result will be tested against the state object.

Navigate over to Calculator.spec.js, add the describe block along with the tests:

describe('setOperator', () => {
  let wrapper;

  beforeEach(() => wrapper = shallow(<Calculator />));

  it('updates the value of selectedOperator', () => {
    wrapper.instance().setOperator('+');
    expect(wrapper.state('selectedOperator')).toEqual('+');
    wrapper.instance().setOperator('/');
    expect(wrapper.state('selectedOperator')).toEqual('/');
  });

  it('updates the value of storedValue to the value of displayValue', () => {
    wrapper.setState({ displayValue: '5' });
    wrapper.instance().setOperator('+');
    expect(wrapper.state('storedValue')).toEqual('5');
  });

  it('updates the value of displayValue to "0"', () => {
    wrapper.setState({ displayValue: '5' });
    wrapper.instance().setOperator('+');
    expect(wrapper.state('displayValue')).toEqual('0');
  });

  it('selectedOperator is not an empty string, does not update storedValue', () => {
    wrapper.setState({ displayValue: '5' });
    wrapper.instance().setOperator('+');
    expect(wrapper.state('storedValue')).toEqual('5');
    wrapper.instance().setOperator('-');
    expect(wrapper.state('storedValue')).toEqual('5');
  });
});

Navigate to Calculator.jsx. Update the setOperator method:

...
class Calculator extends Component {
  ...
  setOperator = value => {
    let { displayValue, selectedOperator, storedValue } = this.state;

    // check if a value is already present for selectedOperator
    if (selectedOperator === '') {
      // update storedValue to the value of displayValue
      storedValue = displayValue;
      // reset the value of displayValue to '0'
      displayValue = '0';
      // update the value of selectedOperator to the given value
      selectedOperator = value;
    } else {
      // if selectedOperator is not an empty string
      // update the value of selectedOperator to the given value
      selectedOperator = value;
    }

    this.setState({ displayValue, selectedOperator, storedValue });
  }
  ...
}

export default Calculator;

Again, all tests are now green. Move on to callOperator.

Call Operator Tests

The callOperator method has no arguments. It updates displayValue, selectedOperator, and storedValue in the state object.

Once again, we need a describe block for callOperator in our Calculator test file. Then, we’ll add our tests for the callOperator method inside. As in the above sections, callOperator will be called from the wrapper.instance() object and the result will be tested against the state object.

Navigate to Calculator.spec.js and add the new describe block at the bottom of the file:

describe('callOperator', () => {
  let wrapper;

  beforeEach(() => wrapper = shallow(<Calculator />));

  it('updates displayValue to the sum of storedValue and displayValue', () => {
    wrapper.setState({ storedValue: '3' });
    wrapper.setState({ displayValue: '2' });
    wrapper.setState({ selectedOperator: '+' });
    wrapper.instance().callOperator();
    expect(wrapper.state('displayValue')).toEqual('5');
  });

  it('updates displayValue to the difference of storedValue and displayValue', () => {
    wrapper.setState({ storedValue: '3' });
    wrapper.setState({ displayValue: '2' });
    wrapper.setState({ selectedOperator: '-' });
    wrapper.instance().callOperator();
    expect(wrapper.state('displayValue')).toEqual('1');
  });

  it('updates displayValue to the product of storedValue and displayValue', () => {
    wrapper.setState({ storedValue: '3' });
    wrapper.setState({ displayValue: '2' });
    wrapper.setState({ selectedOperator: 'x' });
    wrapper.instance().callOperator();
    expect(wrapper.state('displayValue')).toEqual('6');
  });

  it('updates displayValue to the quotient of storedValue and displayValue', () => {
    wrapper.setState({ storedValue: '3' });
    wrapper.setState({ displayValue: '2' });
    wrapper.setState({ selectedOperator: '/' });
    wrapper.instance().callOperator();
    expect(wrapper.state('displayValue')).toEqual('1.5');
  });

  it('updates displayValue to "0" if operation results in "NaN"', () => {
    wrapper.setState({ storedValue: '3' });
    wrapper.setState({ displayValue: 'string' });
    wrapper.setState({ selectedOperator: '/' });
    wrapper.instance().callOperator();
    expect(wrapper.state('displayValue')).toEqual('0');
  });

  it('updates displayValue to "0" if operation results in "Infinity"', () => {
    wrapper.setState({ storedValue: '7' });
    wrapper.setState({ displayValue: '0' });
    wrapper.setState({ selectedOperator: '/' });
    wrapper.instance().callOperator();
    expect(wrapper.state('displayValue')).toEqual('0');
  });

  it('updates displayValue to "0" if selectedOperator does not match cases', () => {
    wrapper.setState({ storedValue: '7' });
    wrapper.setState({ displayValue: '10' });
    wrapper.setState({ selectedOperator: 'string' });
    wrapper.instance().callOperator();
    expect(wrapper.state('displayValue')).toEqual('0');
  });

  it('updates displayValue to "0" if called with no value for storedValue or selectedOperator', () => {
    wrapper.setState({ storedValue: '' });
    wrapper.setState({ displayValue: '10' });
    wrapper.setState({ selectedOperator: '' });
    wrapper.instance().callOperator();
    expect(wrapper.state('displayValue')).toEqual('0');
  });
});

Navigate to Calculator.jsx, then update the callOperator method:

class Calculator extends Component {
  ...
  callOperator = () => {
    let { displayValue, selectedOperator, storedValue } = this.state;
    // temp variable for updating state storedValue
    const updateStoredValue = displayValue;

    // parse strings for operations
    displayValue = parseInt(displayValue, 10);
    storedValue = parseInt(storedValue, 10);

    // performs selected operation
    switch (selectedOperator) {
      case '+':
        displayValue = storedValue + displayValue;
        break;
      case '-':
        displayValue = storedValue - displayValue;
        break;
      case 'x':
        displayValue = storedValue * displayValue;
        break;
      case '/':
        displayValue = storedValue / displayValue;
        break;
      default:
        // set displayValue to zero if no case matches
        displayValue = '0';
    }

    // converts displayValue to a string
    displayValue = displayValue.toString();
    // reset selectedOperator
    selectedOperator = '';
    // check for 'NaN' or 'Infinity', if true set displayValue to '0'
    if (displayValue === 'NaN' || displayValue === 'Infinity') displayValue = '0';

    this.setState({ displayValue, selectedOperator, storedValue: updateStoredValue });
  }
  ...
}

export default Calculator;

The calculator is now fully functional!

final app

All tests should be passing as well!

 PASS  src/components/App/App.spec.js
 PASS  src/components/Keypad/Keypad.spec.js
 PASS  src/components/Key/Key.spec.js
 PASS  src/components/Calculator/Calculator.spec.js
 PASS  src/components/Display/Display.spec.js

Test Suites: 5 passed, 5 total
Tests:       39 passed, 39 total
Snapshots:   5 passed, 5 total
Time:        2.603s
Ran all test suites.

Final Thoughts

At this point we have:

  1. Employed Test-Driven Development, along with Enzyme and Jest, to structure our application and write our tests.
  2. Used CSS Variables to allow for variable reuse and reassignment for responsive design.
  3. Written a reusable React component that we were able to render with individual functions and in multiple styles.
  4. Used React’s PropTypes for type-checking throughout the application.

Next steps:

You may have noticed a quirk if you play with the calculator, that the . key doesn’t work quite as expected. You know what to do: Write a test first, debug, and then write the code to pass the test.

Another quirk you may have come across is that if you click a key following an operation (doesn’t matter which key), the displayValue doesn’t quite update the way we would expect if we are trying to mimic the experience of using an average calculator. Compare this calculator with another calculator, isolate the differences in the experience, write some tests for the new outcomes, and update the calculator functionality to get the tests green.

Try experimenting with the CSS:

new css

After the above items, the next steps could be to add a loading transition or an event listener for keyboard events to the application for a better user experience. If you are curious on how to set up the latter, you can find the completed application in the master branch of the react-calculator repo on GitHub.

Hope you enjoyed the post!

Понравилась статья? Поделить с друзьями:
  • Error exited with error code 128
  • Error expected primary expression at end of input
  • Error exit was not declared in this scope
  • Error exit status 1 gmpu
  • Error exit from the fuse process