Шутка во фронтенд-тестировании

модульный тест

1. Зачем проводить фронтенд-тестирование

Во-первых, я считаю, что front-end тестирование необходимо не для всех проектов, потому что написание тестового кода занимает определенное время, когда проект относительно простой, затраты времени на написание тестового кода могут сказаться на эффективности разработки, но следует отметить, что в процессе разработки интерфейса написание тестового кода имеет следующие преимущества:

  1. Быстрее находите ошибки, пусть подавляющее большинство ошибок будет найдено и устранено на этапе разработки, а также улучшите качество продукта.
  2. Модульные тесты могут быть лучшим выбором, чем написание комментариев.Запуск тестового кода и наблюдение за вводом и выводом иногда может помочь другим понять ваш код лучше, чем комментарии (конечно, важные комментарии все равно должны быть написаны...)
  3. Это полезно для рефакторинга.Если тестовый код проекта хорошо написан, вы можете быстро проверить правильность рефакторинга, проверив, проходит ли тестовый код или нет в процессе рефакторинга, что значительно повышает эффективность рефакторинга.
  4. Процесс написания тестового кода часто позволяет нам глубоко задуматься о бизнес-процессах и сделать код более полным и стандартизированным.

2. Что такоеTDDа такжеBDD

2.1 TDDс модульными тестами

2.1.1 Что такоеTDD

так называемыйTDD(Test Driven Development), то есть разработка через тестирование. Проще говоря, сначала нужно написать тестовый код, а затем написать логический код, чтобы все тестовые коды проходили. Это модель разработки, которая управляет процессом разработки с помощью тестов.

2.1.2 Модульное тестирование

модульное тестирование(unit testing), относится к проверке и проверке наименьшей тестируемой единицы в программном обеспечении. С точки зрения непрофессионала, во внешнем интерфейсе модуль можно понимать как независимый файл модуля, а модульный тест — это проверка такого файла модуля.

Для автономного модуля (ES6модуль), потому что функции относительно независимы, поэтому мы можем сначала написать тестовый код, а затем написать логический код в соответствии с руководством по тестовому коду.

так упоминаетсяTDD, тесты здесь обычно относятся к модульным тестам

2.2 BDDс интеграционными тестами

2.2.1 Что такоеBDD

так называемыйBDD(Behavior Driven Development), то есть разработка, основанная на поведении. Короче говоря, сначала нужно написать код бизнес-логики, а затем написать тестовый код, чтобы заставить всю бизнес-логику выполняться в соответствии с ожидаемыми результатами. Это модель разработки, которая управляет процессом разработки. по поведению пользователя.

2.2.2 Интеграционное тестирование

так называемый интеграционный тест(Integration Testing), относится к осмотру и проверке после того, как все модули программного обеспечения собраны в полную систему в соответствии с проектными требованиями. С точки зрения непрофессионала, на переднем конце интеграционное тестирование можно понимать как тестирование полного интерактивного процесса, реализованного несколькими модулями.

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

так упоминаетсяBDD, тест здесь обычно относится к интеграционному тесту.

3. JestИспользование --- Вводный раздел

3.1 Как мы пишем тестовый код?

Если мы никогда раньше не сталкивались с тестовым кодом, давайте разработаем метод написания тестового кода, как он будет выглядеть? Нам нужно сделать тестовый код простым и понятным, например, давайте возьмем следующий пример:

export function findMax (arr) {
    return Math.max(...arr)
}

Мы написали очень простую функцию для получения максимального значения массива (вам может показаться, что это не строго, но ради простоты мы временно предполагаем, что вход представляет собой непустой числовой массив), если мы напишем тест чтобы эта функция проверяла свою программу на корректность, ее можно было бы представить так:

我期望 findMax([1, 2, 4, 3]) 的结果是 4

Далее переведено на английский:

I expect findMax([1, 2, 4, 3]) to be 4

выраженный процедурным языком,expectВ качестве функции передайте ей объект, который хотите протестировать (findMaxфункция), результат вывода также инкапсулируетсяtoBe(4):

expect(findMax([1, 2, 4, 3])).toBe(4)  // 有内味了

Идя дальше, мы хотим добавить некоторую описательную информацию, такую ​​как

测试findMax函数,我期望 findMax([1, 2, 4, 3]) 的结果是 4

В это время мы можем сделать еще один уровень инкапсуляции и определитьtestфункция, она принимает два параметра, первый параметр — некоторая описательная информация (вот тестfindMaxfunction), второй параметр — это функция, в которой может выполняться вышеописанная логика следующим образом:

test('findMax函数输出', () => {
    expect(findMax([1, 2, 4, 3])).toBe(4) // 内味更深了
})

3.2 Простой самореализующийся тестовый код

Мы можем просто реализоватьtestфункция иexpectфункция, потому что есть цепные вызовыtoBe,такexpectВ конечном итоге функция должна вернутьtoBeОбъект метода, как показано ниже:

// expect函数
function expect (value) {
    return {
        toBe: (toBeValue) => {
            if (toBeValue === value) {
                console.log('测试通过!')
            } else {
                throw new Error('测试不通过!')
            }
        }
    }
}

// test函数
function test (msg, func) {
    try {
        func()
        console.log(`${msg}测试过程无异常!`)
    } catch (err) {
        console.error(`${msg}测试过程出错!`)
    }
}

Наш метод тестирования - это просто проверка чисел. В реальных проектах существует множество типов тестов, которые необходимо протестировать. На данный момент мы можем выбрать более зрелые тестовые среды. Нам представлен простой и удобный в использовании, мощный инструмент, этоjest.

4. JestИспользуйте --- Раздел "Начало работы"

4.1 Подготовка

Наши примеры в этом разделе в основном предназначены для ознакомленияjestСамое основное использование, прежде всего, давайте просто создадим демонстрационную среду.

Первый шаг, используйтеnpm init -y(мойnodeверсияv12.14.1,npmверсияv6.13.4) для инициализации проекта

Второй шаг, установкаjest npm install --save-dev jest(Установка может относиться кОфициальный сайт)

Третий шаг, бегnpx jest --initкоманда для создания файла конфигурации jestjest.config.js, мой выбор таков

Четвертый шаг, бегnpm i babel-jest @babel/core @babel/preset-env -DУстановитьbabelи настроить.babelrcследующим образом

{
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current',
        },
      },
    ],
  ],
};

Пятый шаг, создайте корневой каталогsrcпапку, создайте два новых файлаbasic.jsа такжеbasic.test.js

Шаг 6,package.jsonДобавьте команду:

 "scripts": {
    "test": "jest"
  },

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

4.2 Самое основноеjestПрименение

Далее мы используемTDDДобавьте модульные тесты, чтобы изучить основы использования шутки:

Первый вbasic.jsОпределите две функции полезности в

// 1. 寻找最大值
export function findMax (arr) {
    
}

// 2. 给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的那 两个 整数,如果存在,返回true,否则返回false
export function twoSum (nums, target) {

};

так как этоTDD, мы сначала пишем тестовый код, в процессе постепенно изучаем различныеjestбазовое использование. тестовый код вbasic.test.jsНапишите в файле:

import { findMax, twoSum } from './basic'

// 期望findMax([2, 6, 3])执行后结果为6
test('findMax([2, 6, 3])', () => {
    expect(findMax([2, 6, 3])).toBe(6)
})

// 期望twoSum([2, 3, 4, 6], 10)执行后结果为true
test('twoSum([2, 3, 4, 6], 10)', () => {
    expect(twoSum([2, 3, 4, 6], 10)).toBe(true)
})

Из приведенного выше кода мы видим, чтоjestМетод написания тестового кода такой же, как и то, что мы писали сами до этого (разумеется, изначально это была имитацияjest), в этот момент мы запускаемnpm testкоманду и наблюдайте за выводом командной строки следующим образом:

Обратите внимание на часть в моей красной коробке,ExpectedПредставляет результат ожидаемого выполнения функции, т.е.toBeзначение в ,ReceivedПредставляет результат, полученный фактической функцией выполнения, поскольку мы еще не написали бизнес-код, поэтомуReceivedобеundefined, и, наконец, показать общее количество1тестовые файлы (Test Suites)а также2тестовый код, все они терпят неудачу.

Далее мы улучшаемbasic.jsлогика в

// 1. 寻找最大值
export function findMax (arr) {
    return Math.max(...arr)
}

// 2. 给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的那 两个 整数,如果存在,返回true,否则返回false
export function twoSum (nums, target) {
    for (let i = 0; i < nums.length - 1; i++) {
       for (let j = i + 1; j < nums.length; j++) {
           if (nums[i] + nums[j] === target) {
               return true
           }
       } 
    }
    return false
};

тогда мы снова бежимnpm test, результат следующий

Мы видим, что все тестовые случаи пройдены (интуитивно они все зеленые). Этот вид процесса, когда все тестовые примеры сначала терпят неудачу (красный), и по мере того, как наш процесс разработки продвигается шаг за шагом, окончательный тестовый код проходит процесс (зеленый), то естьTDDи процесс разработки модульных тестов.

4.3 Ещеjest matchers

Как и в предыдущем разделе, вexpectРезультат суждения, следующего за функциейtoBeсуществуетjestизвестный какmatcher, мы представим некоторые другие часто используемыеmatchers

4.3.1 toEqual

Давайте сначала преобразуемtwoSumфункция, которая возвращает массив индексов двух найденных чисел (leetcodeПервый вопрос)

// 2. 给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的那 两个 整数,
// 并返回他们的数组下标(假设每种输入只会对应一个答案,数组中同一个元素不能使用两遍)。
export function twoSum (nums, target) {
    for (let i = 0; i < nums.length - 1; i++) {
       for (let j = i + 1; j < nums.length; j++) {
           if (nums[i] + nums[j] === target) {
               return [i, j]
           }
       } 
    }
    return []
};

В следующей части тестового кода мы оставляем только паруtwoSumФункциональный тест и синхронное изменение тестового кода

test('twoSum([2, 3, 4, 6], 10)', () => {
    expect(twoSum([2, 3, 4, 6], 10)).toBe([2, 3])
})

Мы ожидаем, что результат выполнения функции будет[2, 3]Такой массив, который выглядит нормально, работаетnpm test

Мы обнаружили, что тест не пройден, потому что,toBeДанные базового типа можно судить, но нет способа судить о ссылочных типах, таких как массивы и объекты.В настоящее время нам нужно использоватьtoEqual

test('twoSum([2, 3, 4, 6], 10)', () => {
    expect(twoSum([2, 3, 4, 6], 10)).toEqual([2, 3])
})

изменить наtoEqualПосле этого тестовый код успешно

4.3.2 Некоторые из них, связанные с суждением об истинности и ложности логики.matchers

Эта часть контента очень проста и больше, поэтому прямо прокомментирована в коде:

test('变量a是否为null', () => {
    const a = null
    expect(a).toBeNull()
})

test('变量a是否为undefined', () => {
    const a = undefined
    expect(a).toBeUndefined()
})

test('变量a是否为defined', () => {
    const a = null
    expect(a).toBeDefined()
})

test('变量a是否为true', () => {
    const a = 1
    expect(a).toBeTruthy()
})

test('变量a是否为false', () => {
    const a = 0
    expect(a).toBeFalsy()
})

Результаты теста следующие:

4.3.3 notмодификатор

очень простой,notвот такmatcherотрицание

test('test not', () => {
    const temp = 10
    expect(temp).not.toBe(11)
    expect(temp).not.toBeFalsy()
    expect(temp).toBeTruthy()
})

Результаты теста следующие:

4.3.4 Судить по некоторым числамmatchers

Эта часть контента очень проста и больше, поэтому прямо прокомментирована в коде:

// 判断数num是否大于某个数
test('toBeGreaterThan', () => {
    const num = 10
    expect(num).toBeGreaterThan(7)
})

// 判断数num是否大于等于某个数
test('toBeGreaterThanOrEqual', () => {
    const num = 10
    expect(num).toBeGreaterThanOrEqual(10)
})

// 判断数num是否小于某个数
test('toBeLessThan', () => {
    const num = 10
    expect(num).toBeLessThan(20)
})

// 判断数num是否小于等于某个数
test('toBeLessThanOrEqual', () => {
    const num = 10
    expect(num).toBeLessThanOrEqual(10)
    expect(num).toBeLessThanOrEqual(20)
})

Результаты теста следующие:

Все вышеперечисленное — целочисленные суждения, которые очень просты, но если это суждение, связанное с числами с плавающей запятой, оно будет другим.Например, мы знаем, что0.1 + 0.2 = 0.3Это уравнение не проблема в математике, но на компьютере, потому что проблемы точности, это0.1 + 0.2Результат при использованииtoBeРезультаты не точны0.3, если мы хотим судить о равенстве чисел с плавающей запятой, вjestобеспечиваетtoBeCloseToизmatcherМожно решить:

test('toBe', () => {
    const sum = 0.1 + 0.2
    expect(sum).toBe(0.3)
})

test('toBeCloseTo', () => {
    const sum = 0.1 + 0.2
    expect(sum).toBeCloseTo(0.3)
})

Приведенные выше результаты испытаний следующие:

4.3.5 Строковый матчtoMatch

этоmatcherиспользуется, чтобы определить, совпадает ли строка сtoMatchПредоставленный шаблон соответствует следующим образом:

// 字符串相关
test('toMatch', () => {
    const str = 'Lebron James'
    expect(str).toMatch(/Ja/)
    expect(str).toMatch('Ja')
})

4.3.6 Массивы, связанные с коллекциямиmatchers

можно использоватьtoContainЧтобы определить, содержит ли массив или коллекция элемент, используйтеtoHaveLengthДля определения длины массива используется следующий код:

test('Array Set matchers', () => {
    const arr = ['Kobe', 'James', 'Curry']
    const set = new Set(arr)
    expect(arr).toContain('Kobe')
    expect(set).toContain('Curry')
    expect(arr).toHaveLength(3)
})

4.3.7 Связанные с исключениямиmatchers

использоватьtoThrowЧтобы определить, является ли выброшенное исключение ожидаемым:

function throwError () {
    throw new Error('this is an error!!')
}
test('toThrow', () => {
    expect(throwError).toThrow(/this is an error/)
})

5. jestРасширенное использование

5.1 Групповые тесты и функции ловушек

Суть так называемого группового теста состоит в том, чтобы сгруппировать различные тесты, а затем объединить функцию ловушки (функцию жизненного цикла) для завершения индивидуального теста различных групп для удовлетворения сложных требований процесса тестирования.

мы первыеsrcСоздайте два новых файлаhook.jsа такжеhook.test.js, эта часть кода выполняется в этих двух файлах, сначала заданных непосредственноhook.jsкод

// hook.js
export default class Count {
    constructor () {
        this.count = 2
    }
    increase () {
        this.count ++
    }

    decrease () {
        this.count --
    }

    double () {
        this.count *= this.count
    }

    half () {
        this.count /= this.count
    }
} 

Теперь мы хотимCountЧетыре метода класса проверяются отдельно, и данные не влияют друг на друга, конечно, мы можем сами напрямую его инстанцировать.4Объекты, однако,jestДает нам более элегантный способ записи --- группировка, мы используемdescribeФункции сгруппированы следующим образом:

describe('分别测试Count的4个方法', () => {
    test('测试increase', () => {
        
    })
    test('测试decrease', () => {
        
    })
    test('测试double', () => {
        
    })
    test('测试half', () => {
        
    })
})

Поэтому мы используемdescribeфункция подходитtestРазделите тест на четыре группы, далее, чтобы лучше контролировать каждуюtestгруппа, мы будем использоватьjestфункция крючка. То, что мы собираемся представить здесь, этоjestЧетыре функции крючка вbeforeEach,beforeAll,afterEach,afterAll.

Как подсказывает название,beforeEachвызывается перед выполнением каждой тестовой функции;afterEachОн вызывается после выполнения каждой тестовой функции;beforeAllвызывается перед выполнением всех тестовых функций;afterAllОн вызывается после выполнения всех тестовых функций. Мы можем увидеть следующий пример:

import Count from "./hook"

describe('分别测试Count的4个方法', () => {
    let count
    beforeAll(() => {
        console.log('before all tests!')
    })

    beforeEach(() => {
        console.log('before each test!')
        count = new Count()
    })

    afterAll(() => {
        console.log('after all tests!')
    })

    afterEach(() => {
        console.log('after each test!')
    })

    test('测试increase', () => {
        count.increase()
        console.log(count.count)
    })
    test('测试decrease', () => {
        count.decrease()
        console.log(count.count)
    })
    test('测试double', () => {
        count.double()
        console.log(count.count)
    })
    test('测试half', () => {
        count.half()
        console.log(count.count)
    })
})

Результат вывода показан на рисунке:

Как видите, перед выполнением каждого тестаbeforeEachвоссоздан внутриcount, поэтому счет каждый раз разный. Разумное использование функций ловушек, мы можем лучше настроить тест.

5.2 Таймер для тестирования асинхронного кода

Во время нашей фронтенд-разработки, из-заjavascriptОн однопоточный.Асинхронное программирование часто делается нашими разработчиками, асинхронный код также является наиболее подверженным ошибкам местом.Необходимо проверить логику асинхронного кода.В этом разделе будет обсуждатьсяjestКак выполнить асинхронное тестирование, сделайте подробное введение.

5.2.1 Из самого простогоsetTimeoutНачинать

Сначала мы создаем новыйtimeout.js,timeout.test.jsдокумент,timeout.jsКод файла прост:

export default (fn) => {
    setTimeout(() => {
       fn()
    }, 2000)
}

Наша текущая цель — проверить, будет ли написанная функция передавать функцию в качестве параметра, как мы ожидали (в основном просто, без проверки параметров),2sПосле этого выполните эту функцию.

Наш тестовый код (timeout.test.js)следующим образом:

import timeout from './timeout'

test('测试timer', () => {
    timeout(() => {
        expect(2+2).toBe(4)
    })
})

Если мы запустим этот тестовый код, он пройдет, но на самом деле это означает, что мы написали вtimeoutПроверка метода прошла? мы вtimout.jsраспечатать кусок текста

export default (fn) => {
    setTimeout(() => {
       fn()
       console.log('this is timeout!')
    }, 2000)
}

Затем мы запускаем тестовый код (npm test timeout.testЭто запускает только один файл), вы обнаружите, что ничего не печатается:

На самом деле причина этого явления очень проста.jestВо время запуска тестового кода выполнитеtestметод, выполняемый с первой строки внутри функции до последней строки, когда логика выполнения достигает последней строки блока кода, ни одно исключение не вернет успех теста, в этом процессе不会去等待异步代码的执行结果, поэтому мы тестируем подобные методы независимо отsetTimeoutКак реализовать это в функции обратного вызова и как реализовать это в функции обратного вызова не будет выполнять логику внутри функции обратного вызова.

Если нам нужно, чтобы тестовый код возвращал результат теста после фактического выполнения асинхронной логики в таймере, нам нужно датьtestФункция обратного вызова метода проходит вdoneпараметры, а вtestВызовите это в коде, который выполняется асинхронно внутри методаdoneметод, как это,testметод будет ждать, покаdoneРезультат теста возвращается только после выполнения содержимого блока кода:

import timeout from './timeout'

test('测试timer', (done) => {
    timeout(() => {
        expect(2+2).toBe(4)
        done()
    })
})

Мы видим, что увеличиваетсяdoneПосле параметров получается ожидаемый результат, и распечатывается содержимое, что доказывает выполнение кода в нашей callback-функции.

5.2.2 ИспользованиеfakeTimersПовышение эффективности тестирования

В предыдущем разделе мы представили, как тестировать выполнение асинхронного кода, написанного на таймере, но здесь есть проблема: например, нашему таймеру могут потребоваться десятки секунд для выполнения внутренней логики (хотя это бывает редко, в основном зависит от бизнес-требований), наш тестовый код будет долго возвращать результаты, что несомненно сильно снижает эффективность разработки и тестирования.

jestПринимая это во внимание, мы также можем использоватьfakeTimersИмитация реального таймера. этоfakeTimersПри встрече с таймером это позволяет нам сразу пропустить время ожидания таймера и выполнить внутреннюю логику, например, для простоtimeout.test, наш тестовый код можно изменить следующим образом:

  1. Во-первых, мы используемjest.fn()генерироватьjestФункция предоставлена ​​для тестирования, чтобы нам не пришлось потом самим писать callback-функцию
  2. Во-вторых, мы используемjest.useFakeTimers()запуск методаfakeTimers
  3. Наконец, мы можем пройтиjest.advanceTimersByTime()метод, параметр проходит за миллисекунды,jestЭто значение времени будет немедленно пропущено, и вы также можете передатьtoHaveBeenCalledTimes()этоmathcerдля проверки количества вызовов функции.

Полный код выглядит следующим образом:

test('测试timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    // 时间快进2秒
    jest.advanceTimersByTime(2000)
    expect(fn).toHaveBeenCalledTimes(1)
})

Мы по-прежнему получаем ожидаемые результаты теста, обратите внимание на вывод测试timer(12ms)по сравнению с предыдущим测试timer(2021ms), видно, что время задержки таймера действительно пропущено, что повышает эффективность разработки тестов.

5.2.3 Более сложные сценарии таймера

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

Сначала мы преобразуемtimoutКод в нем следующий:

export default (fn) => {
    setTimeout(() => {
       fn()
       console.log('this is timeout outside!')
       setTimeout(() => {
            fn()
           console.log('this is timeout inside!')
       }, 3000)
    }, 2000)
}

Согласно методу написания из предыдущего раздела, наш тестовый код можно преобразовать в:

test('测试timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    // 时间快进2秒
    jest.advanceTimersByTime(2000)
    expect(fn).toHaveBeenCalledTimes(1)
    // 时间快进3秒
    jest.advanceTimersByTime(3000)
    expect(fn).toHaveBeenCalledTimes(2)
})

На самом деле все просто, только для первого раза2sпозже после3sПосле выполнения второго таймера, на этот разfnназывался2раз, поэтому нам нужно добавить только две последние строки кода. Результат выполнения следующий:

Мы видим, что выводятся оба результата печати. Но текущая реализация не очень хороша, представьте, если сюда вложено больше таймеров, или мы не знаем, сколько там таймеров, это будет более хлопотно.jestДля этой ситуации предусмотрены два полезных метода:

  1. jest.runAllTimers()

Этот метод полностью соответствует своему названию.После вызова будут запущены все таймеры.Наш код можно изменить следующим образом:

test('测试timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    jest.runAllTimers()
    expect(fn).toHaveBeenCalledTimes(2)
})

Видно, что отпечатки внутри двух таймеров выводятся, иjestВремя ожидания таймера по-прежнему быстро пропускается.

  1. jest.runOnlyPendingTimers()

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

test('测试timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    jest.runOnlyPendingTimers()
    expect(fn).toHaveBeenCalledTimes(1)
})

Как видите, распечатывается только содержимое внешнего таймера. Если мы хотим продолжить выводить содержимое внутреннего таймера, потому что внутренний таймер в это время находится в состоянии ожидания, выполняем его сноваjest.runOnlyPendingTimers()Вот и все.

关于上述内容,有一点需要说明:

Если мы напишем несколькоtestфункции, они оба используютfakeTimers, должно быть вbeforeEachХук вызывается каждый разjest.useFakeTimers(), иначе несколькоtestв функцииfakeTimersбудут одинаковыми, будут мешать друг другу, что приведет к непредвиденным результатам выполнения

beforeEach(() => {
    jest.useFakeTimers()
})

5.3 Запрос данных для тестирования асинхронного кода(promise/async await)

5.3.1 Традиционныйpromiseнаписание

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

Для простоты используемaxios(npm i axios)Эта зрелая библиотека помогает нам выполнять запросы данных. Сначала создайте новыйrequest.js, request.test.jsЭти два файла вrequest.jsФайл запрашивает бесплатный API:

import axios from 'axios'

export const request = fn => {
    axios.get('https://jsonplaceholder.typicode.com/todos/1').then(res => {
        fn(res)
        console.log(res)
    })
}

мы вrequest.test.js, чтобы гарантировать, что тест завершится после выполнения асинхронного кода, как описано ранее, вtestпередается в функцию обратного вызоваdoneПараметры, выполняемые в callback-функцииdone(), код показан ниже:

import { request } from './request'

test('测试request', (done) => {
    request(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
          })
        done()
    })
})

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

Давайте переделаем это сейчасrequest.jsкод, который возвращаетpromise:

export const request = () => {
    return axios.get('https://jsonplaceholder.typicode.com/todos/1')
}

Чтобы проверить вышеуказанный код, мыrequest.test.jsТакже внесите некоторые изменения:

test('测试request', () => {
    return request().then(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
          })
    })
})

Обратите внимание, что приведенное выше письмо не нужно передавать вdoneпараметры, однако, нам нужно использоватьreturnвернуть, если не написаноreturn,Этоjestвоплощать в жизньtestфункция, не будет ждатьpromiseВозврат, в данном случае, при выводе результата теста,thenметод не будет выполняться. Мы можем попробовать следующие два способа записи (изменить"completed": true), первый тест не пройдет, а второй тест пройдет (поскольку промис не возвращает результат):

// 第一种
test('测试request', () => {
    return request().then(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": true
          })
    })
})

// 第二种
test('测试request', () => {
    request().then(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": true
          })
    })
})

Приведенный выше тестовый код также можно записать в следующем виде:

test('测试request', () => {
    return expect(request()).resolves.toMatchObject({
        data: {
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
          }
    })
})

Уведомление,resolvesвернусьrequestВесь возвращаемый контент после выполнения метода мы используемtoMatchObjectэтоmatcher, когда входящий объект может соответствоватьrequestПосле выполнения метода возвращается часть пары ключ-значение объекта, и тест проходит.

5.3.2 Использованиеasync awaitсинтаксический сахар

async awaitПо сутиpromiseСинтаксический сахар для связанных вызовов, наш последний тестовый код в предыдущем разделе, если мы используемasync awaitспособ записи следующим образом:

// 写法一
test('测试request', async () => {
    const res = await request()
    expect(res.data).toEqual({
        "userId": 1,
        "id": 1,
        "title": "delectus aut autem",
        "completed": false
    })
})
// 写法二
test('测试request', async () => {
    await expect(request()).resolves.toMatchObject({
        data: {
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
            }
        })
})

Оба вышеупомянутых способа написания могут пройти тест.

5.3.3 Проверка запросов с ошибками

В нашем реальном проекте нам нужно выполнить обработку ошибок для такого запроса интерфейса, а также нам нужно написать тестовый код для нештатных ситуаций.

мы первыеrequest.jsДобавьте метод:

export const requestErr = fn => {
    return axios.get('https://jsonplaceholder.typicode.com/sda')
}

Здесь запрашивается несуществующий адрес интерфейса, и он вернет404, поэтому наш тестовый код:

test('测试request 404', () => {
    return requestErr().catch((e) => {
        console.log(e.toString())
        expect(e.toString().indexOf('404') > -1).toBeTruthy()
    })
})

Здесь есть на что обратить внимание, следующая картинкаjestОписание с официального сайта:

Вероятно, это означает, что если тестовый код используетcatch, шутка не возвращается к исполнениюcatchсодержание, поэтому нам нужно написатьexpect.assertions(1)Это предложение от имени ожидает выполнения утверждения 1 раз,catchМетод считается утверждением, поэтому при нормальных обстоятельствах он не будет выполняться.catch, здесь будет сообщено об ошибке (выполнено 0 утверждений), когда здесь сообщено об ошибке, это означает, что наш код также сгенерировал исключение, как и ожидалось.

Такой способ записи больше не нужен.removed useless expect.assertions, так что теперь напишите его непосредственно вышеописанным способом, прохождение теста означает, что исключение действительно сгенерировано, как мы и ожидали.

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

test('测试request 404', () => {
    return expect(requestErr()).rejects.toThrow(/404/)
})

здесьrejectsи предыдущий разделresolvesДруг для друга, представляя объект ошибки, сгенерированный методом выполнения, этот объект ошибки выдает404аномальный(toThrow(/404/))

Мы также можем использоватьasync awaitСинтаксический сахар для написания кода для тестирования исключений:

test('测试request 404', async () => {
    await expect(requestErr()).rejects.toThrow(/404/)
})
// 或者可以使用try catch语句写的更完整
test('测试request 404', async () => {
    try {
        await requestErr()
    } catch (e) {
        expect(e.toString()).toBe('Error: Request failed with status code 404')
    }
})

5.4 Моделирование в тестах (mock)данные

Сначала мы создаем новыйmock.js, mock.test.jsдокумент

5.4.1 Использованиеjest.fn()Функция моделирования

первый вmock.jsНапишите функцию:

export const run = fn => {
   return fn('this is run!')
}

На самом деле мы использовали его раньшеjest.fn(), здесь мы идем дальше, чтобы узнать это.

  1. Во-первых, нашfn()Функции могут принимать функцию в качестве параметра, и эта функция — то, что нам нужно.jest.fn()для насmockфункция, мы пишемmock.test.js:
test('测试 jest.fn()', () => {
    const fn = jest.fn(() => {
        return 'this is mock fn 1'
    })
})
  1. Второй,jest.fn()Вы можете инициализировать без передачи параметров, а затем вызвать сгенерированныйmockфункциональныйmockImplementationилиmockImplementationOnceметод изменения содержимого фиктивной функции, разница между этими двумя методами заключается в том,mockImplementationOnceизменится толькоmockфункция один раз:
test('测试 jest.fn()', () => {
    const func = jest.fn()
    func.mockImplementation(() => {
        return 'this is mock fn 1'
    })
    func.mockImplementationOnce(() => {
        return 'this is mock fn 2'
    })
    const a = run(func)
    const b = run(func)
    const c = run(func)
    console.log(a)
    console.log(b)
    console.log(c)
})

Мы видим, что результат выполнения функции в первый раз равенthis is mock fn 2, тогда всеthis is mock fn 1

Точно так же мы можем использоватьmockфункциональныйmockReturnValueа такжеmockReturnValueOnce(一次)способ изменить возвращаемое значение функции:

test('测试 jest.fn()', () => {
    const func = jest.fn()
    func.mockImplementation(() => {
        return 'this is mock fn 1'
    })
    func.mockImplementationOnce(() => {
        return 'this is mock fn 2'
    })
    func.mockReturnValue('this is mock fn 3')
    func.mockReturnValueOnce('this is mock fn 4')
        .mockReturnValueOnce('this is mock fn 5')
        .mockReturnValueOnce('this is mock fn 6')
    const a = run(func)
    const b = run(func)
    const c = run(func)
    const d = run(func)
    console.log(a)
    console.log(b)
    console.log(c)
    console.log(d)
})

Обратите внимание, что метод можно вызывать в цепочке, что удобно для многократного вывода разных возвращаемых значений.

  1. Наконец, мы можем использоватьtoBeCalledWithэтоmatcherЧтобы проверить, соответствуют ли параметры, переданные функции, ожидаемому:
test('测试 jest.fn()', () => {
    const func = jest.fn()
    const a = run(func)
    expect(func).toBeCalledWith('this is run!')
})

5.4.2 Данные, полученные с аналогового интерфейса

Много раз в процессе front-end разработки back-end интерфейс еще не был предоставлен, и нам нужно перейти кmockДанные, возвращаемые интерфейсом.

мы первыеmock.jsНапишите простой код для запроса данных в:

import axios from 'axios'

export const request = fn => {
    return axios.get('https://jsonplaceholder.typicode.com/todos/1')
}

Далее мыmock.test.jsв использованииjest.mock()Моделирование методаaxios,использоватьmockResolvedValueа такжеmockResolvedValueOnceМетод имитирует возвращаемые данные, те же самые,mockResolvedValueOnceМетод изменит возвращаемые данные только один раз:

import axios from 'axios'
import { request } from './mock'

jest.mock('axios')

test('测试request', async () => {
    axios.get.mockResolvedValueOnce({ data: 'Jordan', position: 'SG' })
    axios.get.mockResolvedValue({ data: 'kobe', position: 'SG' })
    await request().then((res) => {
        expect(res.data).toBe('Jordan')
    })
    await request().then((res) => {
        expect(res.data).toBe('kobe')
    })
})

Мы используемjest.mock('axios')использоватьjestимитироватьaxios, тест пройден правильно.

5.5 domСвязанные тесты

domСоответствующий тест на самом деле очень прост, мы сначала создаем новыйdom.js, dom.test.jsДва файла, код такой:

// dom.js
export const generateDiv = () => {
    const div = document.createElement('div')
    div.className = 'test-div'
    document.body.appendChild(div)
}

// dom.test.js
import { generateDiv } from './dom'

test('测试dom操作', () => {
    generateDiv()
    generateDiv()
    generateDiv()
    expect(document.getElementsByClassName('test-div').length).toBe(3)
})

Здесь нужно сделать только одно замечание,jestОперационная средаnode.js,здесьjestиспользоватьjsdomчтобы мы могли написатьdomЛогика тестирования, связанная с действием.

5.6 Снимки (snapshot)тестовое задание

Если мы не подвергались тестированию моментальных снимков, мы можем подумать, что имя очень высокое. Итак, мы сначала создаем новыйsnapshot.js, shapshot.test.jsДавайте посмотрим, что на самом деле представляет собой снэпшот-тест.

В нашей ежедневной разработке мы всегда пишем какой-то код конфигурации.В основном они не меняются, но будут небольшие изменения.Такая конфигурация может быть следующей(snapshot.js):

export const getConfig = () => {
    return {
        server: 'https://demo.com',
        port: '8080'
    }
}

Наш тестовый код выглядит следующим образом:

import { getConfig } from './snapshot'

test('getConfig测试', () => {
    expect(getConfig()).toEqual({
        server: 'https://demo.com',
        port: '8080'
    })
})

Итак, мы прошли испытание. Однако, если наша конфигурация изменится позже, мне нужно будет синхронно изменить тестовый код, что будет более проблематично, поэтому,jestНам представлен тестовый снимок, начнем с тестового кода:

test('getConfig测试', () => {
    expect(getConfig()).toMatchSnapshot()
})

После того, как мы запустим тестовый код, он сгенерирует__snapshots__папка, естьsnapshot.test.js.snapФайл Snapshot, содержимое файла выглядит следующим образом:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`getConfig测试 1`] = `
Object {
  "port": "8080",
  "server": "https://demo.com",
}
`;

jestбудет работатьtoMatchSnapshot()Когда мы сначала проверяем, есть ли этот файл снапшота, если нет, то он будет сгенерирован.Когда мы изменим содержимое конфигурации, например, поставимportизменить на8090, снова запустите тестовый код, тест не пройден, результаты следующие:

На данный момент нам просто нужно запуститьnpm test snapshot.test -- -u, мы можем автоматически обновить наш файл снапшота, и тест снова пройдет, что избавляет нас от необходимости вручную обновлять код теста каждый раз при изменении файла конфигурации, что повышает эффективность разработки тестов:

На данный момент наш файл снимка обновляется до следующего кода:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`getConfig测试 1`] = `
Object {
  "port": "8090",
  "server": "https://demo.com",
}
`;

6. jestнекоторые другие полезные знания

6.1 ПустьjestОтслеживание изменений файлов

Эта функция проста, нам просто нужно запуститьjestПри заказе добавьте--watchВот и все, мыpackage.jsonДобавьте новую команду в:

"scripts": {
    "test": "jest",
    "test-watch": "jest --watch"
},

После добавления этой команды, чтобы разрешитьjestМожет отслеживать изменения файлов, нам также нужно превратить наш файл кода вgitсклад,jestтакже официально полагаться наgitВозможность отслеживать изменения файлов, запускаемgit init, то бежимnpm run test-watch, через определенное время включаем режим мониторинга, последние несколько строк вывода командной строки должны быть такими:

Прямо здесьwatchКраткое введение в несколько полезных функций режима (то есть английское описание на рисунке):

  1. согласно сaключ для запуска всего тестового кода
  2. согласно сfключ, чтобы просто запустить весь неудачный тестовый код
  3. согласно сpключ для фильтрации тестового кода по имени файла (регулярная поддержка)
  4. согласно сtКлюч для фильтрации тестового кода по имени теста (регулярная поддержка)
  5. согласно сqзапуск с клавиатурыwatchмодель
  6. согласно сenterклавиша запускает тестовый прогон

Я рекомендую вам попробовать их самостоятельно, все они очень просты и удобны в использовании.

6.2 Создание файлов тестового покрытия

Простыми словами, тестовое покрытие — это доля написанного тестового кода в нашем бизнес-коде.jestПредоставляет нам метод для непосредственного создания файлов тестового покрытия, то есть запускаjestзатем следует команда--coverageпараметры, мы модифицируемpackage.jsonФайлы следующие:

"scripts": {
    "test": "jest",
    "test-watch": "jest --watch",
    "coverage": "jest --coverage"
},

Далее запуститеnpm run coverage, мы можем увидеть вывод командной строки следующим образом:

Это таблица тестового покрытия. В то же время мы обнаружили, что под папкой автоматически создается папкаcoverageпапка:

мы запускаем в браузереindex.html,Как показано ниже:

На этой странице показано тестовое покрытие различных файлов в проекте. Мы можем щелкнуть по имени файла, чтобы увидеть, какие коды были протестированы, а какие нет в конкретном файле.

Вот краткое описание этого элемента таблицы:

  1. Statementsэто покрытие операторов: оно указывает, сколько выполненных операторов в коде тестируется

  2. Branchesпокрытие ветвей: указывает, сколько в кодеif else switchветка тестируется

  3. Functionsэто покрытие функций: оно представляет собой долю функций, которые тестируются в коде

  4. Linesэто покрытие строк: указывает долю проверенных строк в коде

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

6.3 О программеjest.config.jsконфигурационный файл

Мое предложение для изучения файла конфигурации инструмента состоит в том, чтобы сначала следовать файлу по умолчанию, а когда вам нужно изменить конфигурацию, а затем перейти к официальной документации, чтобы изучить ее, не рекомендуется запоминать ее наизусть.

Я не буду представлять, как его настроить здесь.jestфайл, мы можем передать тот, который создается по умолчанию при инициализации jestjest.config.jsчтобы узнать (с подробными примечаниями), или наОфициальный сайтСм. соответствующие параметры конфигурации в .

7. Пишите в конце

Из-за нехватки места нецелесообразно вводить больше информации, большеapiДля получения соответствующей информации рекомендуется обратитьсяОфициальный сайтучиться.

Эта статья лично считает, чтоjestОсновное и основное содержаниеreact(enzyme), vue( @vue/test-utils)Такая структура разработки с использованиемwebpackТакие инженерные средства при использованииjestКогда я буду использовать его, я буду использовать некоторые библиотеки с открытым исходным кодом в сочетании.Я считаю, что хорошо изучил это.jestПосле себя их не должно быть слишком сложно настроить и использовать.

Наконец, я надеюсь, что эта статья поможет всем, и спасибо каждому маленькому партнеру, который может видеть здесь.