Как да започнете да тествате модула на вашия JavaScript код

Всички знаем, че трябва да пишем модулни тестове. Но е трудно да се знае откъде да започнем и колко време да отделим на тестовете в сравнение с реалното изпълнение. И така, откъде да започна? И дали става въпрос само за тестване на код или модулните тестове имат ли други предимства?

В тази статия ще обясня различните видове тестове и какви са предимствата на модулното тестване за екипите за разработка. Ще покажа Jest - рамка за тестване на JavaScript.

Различни видове тестване

Преди да се впуснем в спецификите на модулното тестване, искам да направя бърза проверка на различните видове тестове. Около тях често има някакво объркване и не съм изненадан. Понякога границата между тях е доста тънка.

Единични тестове

Единичните тестове тестват само една част от вашата реализация. Единица. Няма зависимости или интеграции, няма специфични рамки. Те са като метод, който връща връзка на определен език:

export function getAboutUsLink(language){ switch (language.toLowerCase()){ case englishCode.toLowerCase(): return '/about-us'; case spanishCode.toLowerCase(): return '/acerca-de'; } return ''; }

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

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

Това изпълнение трябва да бъде тествано чрез интеграционни тестове. Те обикновено имат по-сложна настройка, която включва подготовка на среди за тестване, инициализиране на зависимости и т.н.

Функционални тестове

Единичните тестове и тестовете за интеграция ви дават увереност, че приложението ви работи. Функционалните тестове разглеждат приложението от гледна точка на потребителя и тестват дали системата работи според очакванията.

В диаграмата по-горе виждате, че модулните тестове формират голямата база на пакета за тестване на вашето приложение. Обикновено те са малки, има много от тях и се изпълняват автоматично.

Така че сега нека да влезем в модулни тестове малко по-подробно.

Защо трябва да обезпокоявам тестовете за писане?

Винаги, когато попитам разработчиците дали са написали тестове за тяхното приложение, те винаги ми казват: „Нямах време за тях“ или „Нямам нужда от тях, знам, че работят“.

Затова се усмихвам учтиво и им казвам това, което искам да ви кажа. Единичните тестове не са само тестване. Те ви помагат и по други начини, така че можете:

Бъдете уверени, че вашият код работи. Кога за последен път сте извършили промяна на кода, вашето изграждане е неуспешно и половината от приложението ви е спряло да работи? Моята беше миналата седмица.

Но това все пак е ОК. Истинският проблем е, когато компилацията успее, промяната се внедри и приложението ви започне да бъде нестабилно.

Когато това се случи, започвате да губите доверие в кода си и в крайна сметка просто се молите приложението да работи. Единичните тестове ще ви помогнат да откриете проблеми много по-рано и да спечелите увереност.

Вземете по-добри архитектурни решения. Промените в кода, но някои решения относно платформата, модулите, структурата и други трябва да бъдат взети по време на ранните етапи на проекта.

Когато започнете да мислите за модулно тестване още в началото, това ще ви помогне да структурирате кода си по-добре и да постигнете правилно разделяне на проблемите. Няма да се изкушите да възлагате множество отговорности на единични кодови блокове, тъй като това би било кошмар за единичен тест.

Точна функционалност преди кодиране. Написвате подписа на метода и започвате да го прилагате веднага. О, но какво трябва да се случи в случай, че даден параметър е нулев? Ами ако стойността му е извън очаквания диапазон или съдържа твърде много знаци? Изхвърляте ли изключение или връщате null?

Единичните тестове ще ви помогнат да откриете всички тези случаи. Погледнете отново въпросите и ще откриете, че точно това определя вашите модулни тестови случаи.

Сигурен съм, че има много повече предимства от писането на модулни тестове. Това са само тези, които си спомням от моя опит. Тези, които научих по трудния начин.

Как да напишете първия си модулен тест на JavaScript

Но да се върнем на JavaScript. Ще започнем с Jest, който е рамка за тестване на JavaScript. Това е инструмент, който позволява автоматично тестване на модули, осигурява покритие на кода и ни позволява лесно да се подиграваме на обекти. Jest също има разширение за Visual Studio Code, достъпно тук.

Има и други рамки, ако се интересувате, можете да ги проверите в тази статия.

npm i jest --save-dev 

Нека използваме споменатия по-горе метод getAboutUsLinkкато изпълнение, което искаме да тестваме:

const englishCode = "en-US"; const spanishCode = "es-ES"; function getAboutUsLink(language){ switch (language.toLowerCase()){ case englishCode.toLowerCase(): return '/about-us'; case spanishCode.toLowerCase(): return '/acerca-de'; } return ''; } module.exports = getAboutUsLink; 

Поставих това във index.jsфайла. Можем да пишем тестове в един и същ файл, но добра практика е отделните тестове да бъдат разделени в специален файл.

Общите модели на именуване включват {filename}.test.jsи {filename}.spec.js. Използвах първата index.test.js,:

const getAboutUsLink = require("./index"); test("Returns about-us for english language", () => { expect(getAboutUsLink("en-US")).toBe("/about-us"); }); 

Първо, трябва да импортираме функцията, която искаме да тестваме. Всеки тест се дефинира като извикване на testфункцията. Първият параметър е името на теста за ваша справка. Другото е функция със стрелка, където извикваме функцията, която искаме да тестваме, и указваме кой резултат очакваме. Аз

В този случай ние извикваме getAboutUsLinkфункция с en-USкато езиков параметър. Очакваме резултатът да бъде /about-us.

Сега можем да инсталираме Jest CLI глобално и да стартираме теста:

npm i jest-cli -g jest 

Ако видите грешка, свързана с конфигурацията, уверете се, че package.jsonфайлът ви е налице. В случай че не го направите, генерирайте такъв с помощта на npm init.

Трябва да видите нещо подобно:

 PASS ./index.test.js √ Returns about-us for english language (4ms) console.log index.js:15 /about-us Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 2.389s 

Great job! This was the first simple JavaScript unit test from start to end. If you installed the Visual Studio Code extension, it will run tests automatically once you save a file. Let's try it by extending the test with this line:

expect(getAboutUsLink("cs-CZ")).toBe("/o-nas"); 

Once you save the file, Jest will inform you that the test failed. That helps you discover potential issues even before committing your changes.

Testing Advanced Functionality and Mocking Services

In real life, the language codes for the getAboutUsLink method would not be constants in the same file. Their value is typically used throughout the project so they would be defined in their own module and imported into all functions that use them.

import { englishCode, spanishCode } from './LanguageCodes' 

You can import these constants into the test the same way. But the situation will get more complicated if you're working with objects instead of simple constants. Take a look at this method:

import { UserStore } from './UserStore' function getUserDisplayName(){ const user = UserStore.getUser(userId); return `${user.LastName}, ${user.FirstName}`; } 

This method uses imported UserStore:

class User { getUser(userId){ // logic to get data from a database } setUser(user){ // logic to store data in a database } } let UserStore = new User(); export { UserStore } 

In order to properly unit test this method, we need to mock UserStore. A mock is a substitute for the original object. It allows us to separate dependencies and real data from the tested method's implementation just like dummies help with crash tests of cars instead of real people.

If we didn't use the mock, we'd be testing both this function and the store. That would be an integration test and we would likely need to mock the used database.

Mocking a Service

To mock objects, you can either provide a mocking function or a manual mock. I will focus on the latter as I have a plain and simple use-case. But feel free to check out other mocking possibilities Jest provides.

jest.mock('./UserStore', () => ({     UserStore: ({         getUser: jest.fn().mockImplementation(arg => ({             FirstName: 'Ondrej',             LastName: 'Polesny'         })), setUser: jest.fn()     }) })); 

First, we need to specify what are we mocking - the ./UserStore module. Next, we need to return the mock that contains all exported objects from that module.

In this sample, it's only the User object named UserStore with the function getUser. But with real implementations, the mock may be much longer. Any functions you don't really care about in the scope of unit testing can be easily mocked with jest.fn().

The unit test for the getUserDisplayName function is similar to the one we created before:

test("Returns display name", () => {     expect(getUserDisplayName(1)).toBe("Polesny, Ondrej"); }) 

As soon as I save the file, Jest tells me I have 2 passing tests. If you're executing tests manually, do so now and make sure you see the same result.

Code Coverage Report

Now that we know how to test JavaScript code, it's good to cover as much code as possible with tests. And that is hard to do. In the end, we're just people. We want to get our tasks done and unit tests usually yield an unwanted workload that we tend to overlook. Code coverage is a tool that helps us fight that.

Code coverage will tell you how big a portion of your code is covered by unit tests. Take for example my first unit test checking the getAboutUsLink function:

test("Returns about-us for english language", () => {    expect(getAboutUsLink("en-US")).toBe("/about-us"); }); 

It checks the English link, but the Spanish version stays untested. The code coverage is 50%. The other unit test is checking the getDisplayName function thoroughly and its code coverage is 100%. Together, the total code coverage is 67%. We had 3 use cases to test, but our tests only cover 2 of them.

To see the code coverage report, type the following command into the terminal:

jest --coverage 

Or, if you're using Visual Studio Code with the Jest extension, you can run the command (CTRL+SHIFT+P) Jest: Toggle Coverage Overlay. It will show you right in the implementation which lines of code are not covered with tests.

By running the coverage check, Jest will also create an HTML report. Find it in your project folder under coverage/lcov-report/index.html.

Now, I don't have to mention that you should strive for 100% code coverage, right? :-)

Summary

In this article, I showed you how to start with unit testing in JavaScript. While it's nice to have your code coverage shine at 100% in the report, in reality, it's not always possible to (meaningfully) get there. The goal is to let unit tests help you maintain your code and ensure it always works as intended. They enable you to:

  • clearly define implementation requirements,
  • better design your code and separate concerns,
  • discover issues you may introduce with your newer commits,
  • and give you confidence that your code works.

The best place to start is the Getting started page in the Jest documentation so you can try out these practices for yourself.

Do you have your own experience with testing code? I'd love to hear it, let me know on Twitter or join one of my Twitch streams.