Ale opravdu, co je to maketa JavaScriptu?

Toto je skvělé pokračování Ale opravdu, co je JavaScripttest? Tak jdeme na to!


Krok 0

Abychom se dozvěděli o simulacích, musíme mít co testovat a co zesměšňovat, proto je modul, který dnes otestujeme:

// thumb-war.js
import {getWinner} from './utils'

function thumbWar(player1, player2) {
  const numberToWin = 2
  let player1Wins = 0
  let player2Wins = 0
  while (player1Wins < numberToWin && player2Wins < numberToWin) {
    const winner = getWinner(player1, player2)
    if (winner === player1) {
      player1Wins++
    } else if (winner === player2) {
      player2Wins++
    }
  }
  return player1Wins > player2Wins ? player1 : player2
}

export default thumbWar

Je to palcová válečná hra, kde hrajete nejlepší 2 ze tří. Používá funkci nazvanou getWinner od utils. getWinner vrátí vítězného hráče nebo nulu za remízu. Budeme předstírat, že se jedná o volání nějaké třetí strany pro strojové učení služby, která má testovací prostředí, které nekontrolujeme a je nespolehlivé, takže ho chceme pro testy zesměšnit . Toto je jedna z (vzácných) situací, kdy je zesměšňování skutečně vaší jedinou možností, jak spolehlivě otestovat váš kód. (Stále to dělám synchronně, abych náš příklad ještě více zjednodušil).

Kromě toho, pokud znovu neimplementujeme všechny vnitřní funkce getWinner v našich testech neexistuje způsob, jak skutečně učinit užitečná tvrzení, protože vítěz palcové války není deterministický. Takže bez zesměšňování, zde je to nejlepší, co náš test dokáže:

// thumb-war.0.js
import thumbWar from '../thumb-war'

test('returns winner', () => {
  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(['Ken Wheeler', 'Kent C. Dodds'].includes(winner)).toBe(true)
})

Můžeme jen tvrdit, že vítězem je jeden z hráčů, a možná to stačí. Ale pokud opravdu chceme zajistit, aby naše thumbWar funkce se správně integruje s getWinner (v maximální možné míře), pak pro to budeme chtít vytvořit simulaci a prosadit skutečného vítěze.

Krok 1

Nejjednodušší formou zesměšňování je opičí záplatování hodnot. Zde je příklad toho, jak náš test vypadá, když to uděláme:

import thumbWar from '../thumb-war'
import * as utils from '~/utils'

test('returns winner', () => {
  const originalGetWinner = utils.getWinner
  // eslint-disable-next-line import/namespace
  utils.getWinner = (p1, p2) => p2

  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')

  // eslint-disable-next-line import/namespace
  utils.getWinner = originalGetWinner
})

Zde si všimnete několika věcí. Nejprve musíme importovat modul utils jako * import, takže máme objekt, se kterým můžeme manipulovat (POZNÁMKA:čtěte to se zrnem soli! Více o tom, proč je to špatné později). Pak musíme uložit původní funkci na začátku našeho testu a obnovit ji na konci, testy dudlíku nejsou ovlivněny změnami, které provádíme v utils modul.

To vše je pouze nastavení pro skutečnou zesměšňující část našich změn. Zesměšňuje řádek, který zní:

utils.getWinner = (p1, p2) => p2

Toto je zesměšňování opice. Je to efektivní (nyní jsme schopni zajistit, že existuje konkrétní vítěz thumbWar hra), ale má to určitá omezení. Jedna věc, která je otravná, je varování eslint, takže jsme to zakázali (opět to ve skutečnosti nedělejte, protože váš kód nevyhovuje specifikacím! Opět, více o tom později). Také ve skutečnosti nevíme jistě, zda utils.getWinner funkce byla volána tolik, kolik měla být (dvakrát, pro nejlepší 2 ze 3 her). To může nebo nemusí být důležité pro aplikaci, ale je to důležité pro to, co se vás snažím naučit, takže to pojďme zlepšit!

Krok 2

Pojďme přidat nějaký kód, abychom se ujistili, že getWinner funkce byla volána dvakrát a ujistěte se, že byla volána se správnými argumenty.

import thumbWar from '../thumb-war'
import * as utils from '~/utils'

test('returns winner', () => {
  const originalGetWinner = utils.getWinner
  // eslint-disable-next-line import/namespace
  utils.getWinner = (...args) => {
    utils.getWinner.mock.calls.push(args)
    return args[1]
  }
  utils.getWinner.mock = {calls: []}

  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utils.getWinner.mock.calls).toHaveLength(2)
  utils.getWinner.mock.calls.forEach(args => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })

  // eslint-disable-next-line import/namespace
  utils.getWinner = originalGetWinner
})

Takže zde přidáváme mock námitky proti naší falešné funkci, abychom si mohli ponechat nějaká falešná metadata o tom, jak se funkce volá. To nám umožňuje přidat tato dvě tvrzení:

expect(utils.getWinner.mock.calls).toHaveLength(2)
utils.getWinner.mock.calls.forEach(args => {
  expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
})

To nám pomáhá zajistit, že naše simulace je volána správně (se správnými argumenty) a že je volána správně kolikrát (dvakrát na dvě hry ze tří).

Nyní, pokud naše simulace dokáže modelovat to, co dělá verze ve skutečném světě, můžeme získat trochu sebedůvěry, že náš kód funguje, i když musíme zesměšňovat to, co getWinner skutečně dělá. Možná není špatný nápad implementovat nějaké testování smluv, které zajistí, že smlouva mezi getWinner a služba třetí strany je udržována pod kontrolou. Ale to nechám na vaší fantazii!

Krok 3

Takže všechny tyhle věci jsou skvělé, ale je otravné neustále sledovat, kdy se volá náš model. Ukázalo se, že to, co jsme udělali, je ruční implementace falešné funkce a Jest je dodáván s vestavěným nástrojem pro přesně toto. Pojďme tedy zjednodušit náš kód tím, že to použijeme!

import thumbWar from '../thumb-war'
import * as utils from '~/utils'

test('returns winner', () => {
  const originalGetWinner = utils.getWinner
  // eslint-disable-next-line import/namespace
  utils.getWinner = jest.fn((p1, p2) => p2)

  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utils.getWinner).toHaveBeenCalledTimes(2)
  utils.getWinner.mock.calls.forEach(args => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })

  // eslint-disable-next-line import/namespace
  utils.getWinner = originalGetWinner
})

Zde jsme jednoduše zabalili naše getWinner simulovaná implementace s jest.fn .Toto efektivně dělá všechny stejné věci, které jsme dělali, kromě toho, že se jedná o speciální funkci Jest zesměšňování, existuje několik speciálních tvrzení, která můžeme použít právě pro tento účel (jako toHaveBeenCalledTimes ). Jest má výraz nazvanýtoHaveBeenNthCalledWith ,tak jsme se mohli vyhnout našemu forEach , ale myslím si, že je to v pořádku tak, jak to je (a naštěstí jsme implementovali vlastní sbírku metadat stejným způsobem, jakým to dělá Jest, takže toto tvrzení nemusíme měnit. Fajn!).

Další věc, kterou nemám rád, je nutnost sledovat originalGetWinner a na konci to obnovte. Taky mi vadí ty eslintovské komentáře, které jsem tam musel dát (pamatujte! To pravidlo je super důležité a promluvíme si o něm za chvíli). Podívejme se, zda můžeme věci dále zjednodušit pomocí dalšího nástroje Jest.

Krok 4

Naštěstí má Jest nástroj s názvem spyOn který dělá přesně to, co potřebujeme:

import thumbWar from '../thumb-war'
import * as utils from '~/utils'

test('returns winner', () => {
  jest.spyOn(utils, 'getWinner')
  utils.getWinner.mockImplementation((p1, p2) => p2)

  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')

  utils.getWinner.mockRestore()
})

Bonbón! Opravdu jsme věci zjednodušili! Mock funkce se také nazývají spies (proto se API pro toto nazývá spyOn ). Ve výchozím nastavení zachová Jest pouze původní implementaci getWinner ale stále sledujte, jak se tomu říká. I když nechceme, aby se původní implementace nazývala sowe use mockImplementation zesměšňovat, co se stane, když se to řekne. Pak na konci použijeme mockRestore abychom po sobě uklidili, jako jsme byli předtím. Hezké, že!?

Takže pamatujete na chyby eslint, které jsme viděli? Pojďme se jim věnovat příště!

Krok 5

Chyba ESLint, kterou jsme viděli, je ve skutečnosti opravdu důležitá. Problém jsme obešli, protože jsme změnili náš kód takovým způsobem, že eslint-plugin-import nebylo možné staticky zjistit, že stále skutečně porušujeme pravidlo. Ale toto pravidlo je ve skutečnosti velmi důležité. Pravidlo je:import/namespace .Důvod, proč je v tomto případě nefunkční, je:

Proč je to tedy problém? Je to proto, že skutečnost, že náš kód funguje, je jen štěstí, jak jej Babel transpiluje do CommonJS a jak funguje požadovaná mezipaměť. Když importuji modul, importuji neměnné vazby na funkce v tomto modulu, takže pokud importuji stejný modul ve dvou různých souborech a pokus o mutaci vazeb, mutace bude platit pouze pro modul, kde k mutaci došlo (ve skutečnosti si tím nejsem jistý, může se mi zobrazit chyba, což by bylo pravděpodobně lepší). Takže pokud na to spoléháte, pravděpodobně jste v boji o upgrade na moduly ES pro realzie.

To znamená, že to, co se chystáme udělat, ve skutečnosti také nesplňuje specifikaci (nějaké kouzlo pro nás dělají testovací nástroje), ale náš kód vypadá vyhovuje specifikaci, která je důležitá, aby se lidé v týmu nenaučili zlozvyky, které by se mohly dostat do kódu aplikace.

Abychom to mohli vyřešit, mohli pokusit se makat pomocí require.cache vyměnit skutečnou implementaci modulu za naši simulovanou verzi, ale zjistili bychom, žeimports dojde před spuštěním našeho kódu, takže bychom jej nemohli spustit včas, aniž bychom jej natáhli do jiného souboru. Také se mé děti chystají probudit a já to musím udělat!

Nyní se tedy dostáváme k jest.mock API. Protože Jest pro nás ve skutečnosti simuluje modulový systém, může velmi snadno a hladce vyměnit simulovanou implementaci modulu za skutečnou! Náš test nyní vypadá takto:

import thumbWar from '../thumb-war'
import * as utilsMock from '~/utils'

jest.mock('~/utils', () => {
  return {
    getWinner: jest.fn((p1, p2) => p2),
  }
})

test('returns winner', () => {
  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utilsMock.getWinner).toHaveBeenCalledTimes(2)
  utilsMock.getWinner.mock.calls.forEach(args => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })
})

Super že jo!? Jen říkáme Jestovi, že chceme, aby všechny soubory místo toho používaly naši simulovanou verzi a fuj! To ano! Všimněte si také, že jsem změnil název importu zutils na utilsMock . Není to povinné, ale rád to dělám, abych sdělil záměr, že by to mělo být importování zesměšněné verze modulu, nikoli skutečné věci.

Běžná otázka:Pokud chcete pouze zesměšňovat jednu z několika funkcí v modulu amodule, může se vám líbit jest.requireActual API.

Krok 6

Dobře, takže jsme skoro hotovi. Co když používáme toto getWinner funguje několik našich testů a my nechceme kopírovat/vkládat tento model všude? Tady je __mocks__ adresář přijde vhod! Vytvoříme tedy __mocks__ adresář hned vedle souboru, který chceme zesměšnit, a poté vytvořte soubor se stejným názvem:

other/whats-a-mock/
├── __mocks__
│   └── utils.js
├── __tests__/
├── thumb-war.js
└── utils.js

Uvnitř __mocks__/utils.js soubor, vložíme toto:

// __mocks__/utils.js
export const getWinner = jest.fn((p1, p2) => p2)

A díky tomu můžeme aktualizovat náš test:

// __tests__/thumb-war.js
import thumbWar from '../thumb-war'
import * as utilsMock from '~/utils'

jest.mock('~/utils')

test('returns winner', () => {
  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utilsMock.getWinner).toHaveBeenCalledTimes(2)
  utilsMock.getWinner.mock.calls.forEach(args => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })
})

🎉 Hurá! Nyní jen řekneme jest.mock(pathToModule) a automaticky si vybere mockfile, který jsme pro nás vytvořili.

Nyní možná nechceme, aby tento model vždy vrátil druhého hráče, takže můžeme použítmockImplementation pro konkrétní testy, abychom ověřili, že to funguje, když vrátíme druhý a pak první a pak znovu druhý atd. Klidně si to vyzkoušejte sami. Pokud chcete, můžete svůj maket vybavit také některými nástroji. Theworld je vaše ústřice.

Hodně štěstí!