Написание тестируемого кода на JavaScript: краткий обзор

Опубликовано: 2022-03-11

Независимо от того, используем ли мы Node в паре с тестовой средой, такой как Mocha или Jasmine, или запускаем DOM-зависимые тесты в безголовом браузере, таком как PhantomJS, наши возможности для модульного тестирования JavaScript сейчас лучше, чем когда-либо.

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

Держите бизнес-логику и логику отображения отдельно

Одной из основных задач браузерного приложения на основе JavaScript является прослушивание событий DOM, инициированных конечным пользователем, а затем реагирование на них путем запуска некоторой бизнес-логики и отображения результатов на странице. Заманчиво написать анонимную функцию, которая выполняет основную часть работы прямо там, где вы настраиваете прослушиватели событий DOM. Проблема, которую это создает, заключается в том, что теперь вам нужно имитировать события DOM для проверки вашей анонимной функции. Это может создать накладные расходы как в строках кода, так и во времени, необходимом для запуска тестов.

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

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

 // hard to test $('button').on('click', () => { $.getJSON('/path/to/data') .then(data => { $('#my-list').html('results: ' + data.join(', ')); }); }); // testable; we can directly run fetchThings to see if it // makes an AJAX request without having to trigger DOM // events, and we can run showThings directly to see that it // displays data in the DOM without doing an AJAX request $('button').on('click', () => fetchThings(showThings)); function fetchThings(callback) { $.getJSON('/path/to/data').then(callback); } function showThings(data) { $('#my-list').html('results: ' + data.join(', ')); }

Используйте обратные вызовы или обещания с асинхронным кодом

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

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

Иллюстрация: использование функции обратного вызова в качестве параметра модульного тестирования

Еще один распространенный и все более популярный способ организации асинхронного кода — это Promise API. К счастью, $.ajax и большинство других асинхронных функций jQuery уже возвращают объект Promise, поэтому многие распространенные варианты использования уже рассмотрены.

 // hard to test; we don't know how long the AJAX request will run function fetchData() { $.ajax({ url: '/path/to/data' }); } // testable; we can pass a callback and run assertions inside it function fetchDataWithCallback(callback) { $.ajax({ url: '/path/to/data', success: callback, }); } // also testable; we can run assertions when the returned Promise resolves function fetchDataWithPromise() { return $.ajax({ url: '/path/to/data' }); }

Избегайте побочных эффектов

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

Иллюстрация: Каскадный эффект, вызванный внешним состоянием.

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

 // hard to test; we have to set up a globalListOfCars object and set up a // DOM with a #list-of-models node to test this code function processCarData() { const models = globalListOfCars.map(car => car.model); $('#list-of-models').html(models.join(', ')); } // easy to test; we can pass an argument and test its return value, without // setting any global values on the window or checking the DOM the result function buildModelsString(cars) { const models = cars.map(car => car.model); return models.join(','); }

Используйте внедрение зависимостей

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

 // depends on an external state database connector instance; hard to test function updateRow(rowId, data) { myGlobalDatabaseConnector.update(rowId, data); } // takes a database connector instance in as an argument; easy to test! function updateRow(rowId, data, databaseConnector) { databaseConnector.update(rowId, data); }

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

Дайте каждой функции единственную цель

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

В функциональном программировании акт объединения нескольких одноцелевых функций называется композицией. В Underscore.js даже есть функция _.compose , которая берет список функций и связывает их вместе, принимая возвращаемое значение каждого шага и передавая его следующей функции в строке.

 // hard to test function createGreeting(name, location, age) { let greeting; if (location === 'Mexico') { greeting = '!Hola'; } else { greeting = 'Hello'; } greeting += ' ' + name.toUpperCase() + '! '; greeting += 'You are ' + age + ' years old.'; return greeting; } // easy to test function getBeginning(location) { if (location === 'Mexico') { return 'Hola'; } else { return 'Hello'; } } function getMiddle(name) { return ' ' + name.toUpperCase() + '! '; } function getEnd(age) { return 'You are ' + age + ' years old.'; } function createGreeting(name, location, age) { return getBeginning(location) + getMiddle(name) + getEnd(age); }

Не изменяйте параметры

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

Иллюстрация: Изменение параметров может вызвать проблемы

Вместо этого, если у вас есть функция, которая принимает объект или массив, пусть она действует на этот объект или массив, как если бы он был доступен только для чтения. Создайте новый объект или массив в коде и добавьте к нему значения в зависимости от ваших потребностей. Или используйте Underscore или Lodash для клонирования переданного объекта или массива перед операцией с ним. Еще лучше использовать такой инструмент, как Immutable.js, который создает структуры данных только для чтения.

 // alters objects passed to it function upperCaseLocation(customerInfo) { customerInfo.location = customerInfo.location.toUpperCase(); return customerInfo; } // sends a new object back instead function upperCaseLocation(customerInfo) { return { name: customerInfo.name, location: customerInfo.location.toUpperCase(), age: customerInfo.age }; }

Пишите тесты перед кодом

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

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

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

Заворачивать

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

Связанный:
  • 10 самых распространенных ошибок, которые допускают разработчики JavaScript
  • Жажда скорости: ретроспектива лучших вызовов JavaScript-кодирования