This is a comprehensive guide to providing reliability in JavaScript and Node.js. Here are collected dozens of the best posts, books and tools.
First, deal with generally accepted testing methods that underlie any application. And then you can delve into the field of interest to you: frontend and interfaces, backend, CI or all of the above.
Content
Section 0. Golden Rule
0. Golden Rule: Stick to Lean Testing
What to do. The test code is different from what goes into operation. Make it as simple as possible, short, free from abstractions, single, wonderful at work and thrifty. The other person should look at the test and immediately understand what he is doing.
Our heads are busy with production code, they do not have free space for extra complexity. If we shove a new portion of complex code into our poor mind, this will slow down the work of the entire team on the task, for the sake of which we are testing. In fact, because of this, many teams simply shun tests.
Tests - this is an opportunity to get a friendly and smiling assistant, with whom it is very good to work and which gives a huge return on small investments. Scientists believe that in our brain there are two systems: one for actions that do not require effort, such as driving on an empty road, and the second for complex operations requiring awareness, such as solving mathematical equations. Create your tests for the first system, so that when you look at the code you get a feeling of simplicity, comparable to editing an HTML document, and not with a
2X(17 × 24)
solution
2X(17 × 24)
.
This can be achieved by carefully selecting the methods, tools and goals for testing, so that they are economical and give a large ROI. Test only as much as necessary, try to be flexible. Sometimes it’s worth dropping some tests and sacrificing reliability for the sake of speed and simplicity.

Most of the recommendations below are derived from this principle.
Ready?Section 1. Anatomy of the test
1.1 The name of each test should consist of three parts
What to do. The test report should indicate whether the current revision of the application meets the requirements of those people who are not familiar with the code: testers involved in the deployment of DevOps engineers, as well as yourself in two years. It will be best if the tests report information in the language of requirements, and their names consist of three parts:
- What exactly is being tested? For example, the
ProductsService.addNewProduct
method. - Under what conditions and scenarios? For example, the price is not passed to the method.
- What is the expected result? For example, a new product is not approved.
Otherwise. The deployment fails, the test called “Add product” fails. Do you understand what exactly works wrong?
Note Each chapter has a code example, and sometimes an illustration. See spoilers.
Code examplesHow to do it right. The name of the test consists of three parts.
1.2 Structure the tests according to the AAA pattern
What to do. Each test should consist of three clearly separated sections: Arrange (preparation), Act (action) and Assert (result). Adhering to such a structure ensures that the reader of your code does not have to use the brain processor to understand the test plan:
Arrange: all the code that brings the system to state according to the test scenario. This may include creating an instance of the module in the test designer, adding records to the database, creating stubs instead of objects, and any other code that prepares the system for the test run.
Act: code execution as part of a test. Usually just one line.
Assert: make sure that the resulting value meets expectations. Usually just one line.
Otherwise. You will not only spend long hours working with the main code, but your brain will swell also from what should be a simple job - from testing.
Code examplesHow to do it right. A test structured according to the AAA pattern.
describe.skip('Customer classifier', () => { test('When customer spent more than 500$, should be classified as premium', () => {
An example of antipattern. No separation, in one piece, is more difficult to interpret.
test('Should be classified as premium', () => { const customerToClassify = {spent:505, joined: new Date(), id:1} const DBStub = sinon.stub(dataAccess, "getCustomer") .reply({id:1, classification: 'regular'}); const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); expect(receivedClassification).toMatch('premium'); });
1.3 Describe expectations in the language of the product: state in BDD style
What to do. Programming tests in a declarative style allows the user to immediately understand the essence without spending a single brain processor cycle. When you write imperative code packed in conditional logic, the reader has to make a lot of effort. From this point of view, you need to describe expectations in a human-like language in a declarative BDD style using expect / should and not using custom code. If in Chai and Jest there is no necessary assertion, which is often repeated, then you can
expand the matcher Jest or write
your own plugin for Chai .
Otherwise. The team will write fewer tests and decorate annoying tests
with .skip()
.
Code examplesAn example using Mocha .
An example of antipattern. To understand the essence of the test, the user is forced to get through a rather long imperative code.
it("When asking for an admin, ensure only ordered admins in results" , ()={
How to do it right. Reading this declarative test is straightforward.
it("When asking for an admin, ensure only ordered admins in results" , ()={
1.4 Follow Black Box Testing: Test Only Public Methods
What to do. Testing the insides will lead to huge overhead and will yield almost nothing. If your code or API provides the correct results, is it worth it to spend three hours testing how it works internally and then supporting these fragile tests? When you check public behavior, you simultaneously implicitly check the implementation itself, your tests will fail only if there is a specific problem (for example, incorrect output). This approach is also called behavioral testing. On the other hand, if you are testing the internals (the “white box” method), instead of planning the output of the components, you will focus on small details, and your tests may break due to small alterations of the code, even if the results are okay, but escort will take much more resources.
Otherwise. Your tests will behave like a
boy shouting “Wolf!” : Loudly report false positives (for example, the test fails due to a change in the name of a private variable). It is not surprising that soon people will begin to ignore CI notifications, and one day they will miss a real bug ...
Code examplesAn example of antipattern. testing the insides for no good reason.
An example using Mocha .
class ProductService{
1.5 Choose the right imitated implementation: avoid fake objects in favor of stubs and spies
What to do. Simulated implementations (test doubles) are a necessary evil, because they are associated with the internals of the application, and some are of great value (
refresh the memory of imitated implementations: fake objects (mocks), stubs (stubs) and spy objects (spies) ) However, not all techniques are equivalent. Spies and stubs are designed to test requirements, but have an inevitable side effect - they also slightly affect the insides. And fake objects are designed to test the insides, which leads to huge overhead, as described in chapter 1.4.
Before using simulated implementations, ask yourself the simplest question: “Do I use this to test functionality that has appeared or may appear in the documentation with requirements?” If not, it smacks of testing using the “white box” method.
For example, if you want to find out if the application behaves as it should when the payment service is unavailable, you can make a stub instead and return “No answer” to check if the module under test returns the correct value. So you can check the behavior / response / output of the application in certain scenarios. You can also confirm with the help of a spy that when the service was unavailable, the letter was sent, it is also behavioral testing, which is better reflected in the documentation with the requirements ("Send a letter if payment information cannot be saved"). At the same time, if you make a fake payment service and make sure that it is called using the correct JS types, then your test is aimed at internals that are not related to the functionality of the application and which are likely to change frequently.
Otherwise. Any code refactoring involves finding and updating all fake objects in the code. Tests from an assistant friend turn into a burden.
Code examplesAn example of antipattern. Fake objects are for guts.
Example using Sinon .
it("When a valid product is about to be deleted, ensure data access DAL was called once, with the right product and right config", async () => {
How to do it right. Spies are designed to test requirements, but there is a side effect - they inevitably affect the insides.
it("When a valid product is about to be deleted, ensure an email is sent", async () => {
1.6 Do not use “foo”, use realistic input
What to do. Often production bugs occur with very specific and surprising input data. The more realistic the data during testing, the more likely to catch bugs on time. To generate pseudo-real data simulating the variety and type of production data, use special libraries, for example,
Faker . Such libraries can generate realistic phone numbers, user nicknames, bank cards, company names, even the text “lorem ipsum”. You can create tests (on top of unit tests, and not instead of them) that randomize fake data to fit a module into a test, or even import real data from a production environment. Want to go even further? Read the next chapter (about property-based testing).
Otherwise. Your development testing will look successful using synthetic inputs like “Foo”, and production data may crash when a hacker
@3e2ddsf . ##' 1 fdsfds . fds432 AAAA
tricky line like
@3e2ddsf . ##' 1 fdsfds . fds432 AAAA
@3e2ddsf . ##' 1 fdsfds . fds432 AAAA
@3e2ddsf . ##' 1 fdsfds . fds432 AAAA
.
Code examplesAn example of antipattern. A test suite that runs successfully due to the use of unrealistic data.
An example using Jest .
const addProduct = (name, price) =>{ const productNameRegexNoSpace = /^\S*$/;
How to do it right. Randomize realistic input.
it("Better: When adding new valid product, get successful confirmation", async () => { const addProductResult = addProduct(faker.commerce.productName(), faker.random.number());
1.7 Use property-based testing to validate multiple input combinations
What to do. Usually for each test we select several samples of input data. Even if the input format is similar to real data (see the chapter “Do not use“ foo ”), we cover only a few combinations of input data (method
('', true, 1)
, method
("string" , false" , 0)
). But in operation, an API that is called with five parameters can be called with thousands of different combinations, one of which can lead to a process
crash (
fuzzing ). What if you could write one test that automatically sends 1000 combinations of input data and fixing, at what combinations the code does not return the correct answer? The same thing we do with m todike test based on the properties:. by sending all possible combinations of input data into the test unit we increase the chance of a bug detection, for example, we have a method
addNewProduct(id, name, isDiscount)
. Supporting his library will call this method with many combinations
(, , )
, for example,
(1, "iPhone", false)
,
(2, "Galaxy", true)
etc. You can test based on properties using your favorite test runner (Mocha, Jest etc.) and libraries like
js-verify or
testcheck (it has much better documentation). You can also
try the fast-check library , which offers additional features and is actively accompanied by the author.
Otherwise. You are thoughtlessly choosing input data for the test, which covers only well-functioning code execution paths. Unfortunately, this reduces the effectiveness of testing as a means of detecting errors.
Code examplesHow to do it right. Test numerous combinations with the mocha-testcheck.
require('mocha-testcheck').install(); const {expect} = require('chai'); const faker = require('faker'); describe('Product service', () => { describe('Adding new', () => {
1.8 Use only short and inline shots if necessary.
What to do. When you need to
test based on snapshots , use only short snapshots without all the extra (for example, in 3-7 lines), including them as part of the test (
Inline Snapshot ), and not as external files. Following this recommendation will keep your tests self-evident and more reliable.
On the other hand, the “classic snapshot” guides and tools provoke us to store large files (for example, markup of component rendering or JSON API results) on external media and compare the results with the saved version every time the test is run. It can, say, implicitly associate our test with 1000 lines containing 3000 values that the author of the test never saw about which he had not expected. Why is that bad? Because there are 1000 reasons for the test to fail. Even one line can invalidate a snapshot, and this can happen often. How much After each space, comment, or minor change in CSS or HTML. In addition, the name of the test will not tell you about the failure, because it only checks that 1000 lines have not changed, and also encourages the author of the test to take as long as desired a long document that he could not analyze and verify. All of these are symptoms of an obscure and hasty test that does not have a clear task and is trying to achieve too much.
It is worth noting that there are several situations in which it is acceptable to use long and external images, for example, when confirming the scheme, and not the data (extracting values and focus on the fields), or when the received documents rarely change.
Otherwise. UI tests fail. The code looks fine, ideal pixels are displayed on the screen, so what happens? Your testing with snapshots just revealed the difference between the original document and the just received one - one space character was added to the markup ...
Code examplesAn example of antipattern. Associating a test with some unknown 2000 lines of code.
it('TestJavaScript.com is renderd correctly', () => {
How to do it right. Expectations are visible and in the spotlight.
it('When visiting TestJavaScript.com home page, a menu is displayed', () => {
1.9 Avoid global test benches and initial data, add data to each test separately
What to do. According to the golden rule (chapter 0), each test should add and work within its own set of rows in the database to avoid bindings, and it was easier for users to understand the test. In reality, testers often violate this rule, before running tests filling the database with initial data (seeds) (
also called a “test bench” ) in order to increase productivity.
And although performance is really an important task, it can decrease (see the chapter “Testing components”), however, the complexity of the tests is much more harmful and it is it that should most often manage our decisions. Almost every test case should explicitly add the necessary records to the database and work only with them. If performance is critical, then as a compromise, you can only fill in the initial data with tests that do not change information (for example, queries).Otherwise. Several tests failed, deployment aborted, now the team will spend precious time, do we have a bug? Let's look, damn it, it seems that two tests changed the same initial data.Code examplesAn example of antipattern. Tests are not independent and use some kind of global hook to get global data from the database. before(() => {
How to do it right. You can stay within the test, each test only works with its own data. it("When updating site name, get successful confirmation", async () => {
1.10 Do not catch errors, but expect them
. , - , try-catch-finally , . ( ), .
Chai:
expect(method).to.throw
( Jest:
expect(method).toThrow()
). , , . , , .
. (, CI-) , .
An example of antipattern. A long test script that tries to catch an error using try-catch. /it("When no product name, it throws error 400", async() => { let errorWeExceptFor = null; try { const result = await addNewProduct({name:'nest'});} catch (error) { expect(error.code).to.equal('InvalidInput'); errorWeExceptFor = error; } expect(errorWeExceptFor).not.to.be.null;
. , , , QA .
it.only("When no product name, it throws error 400", async() => { expect(addNewProduct)).to.eventually.throw(AppError).with.property('code', "InvalidInput"); });
1.11
. :
- smoke-,
- IO-less,
- , , ,
- , pull request', .
, , , #cold #api #sanity. . , Mocha :
mocha — grep 'sanity'
.
. , , , , , , , .
. '#cold-test' (Cold=== , - , ).
1.12
. , Node.js . , Node.
TDD . , , , .
-- , - . , , . , . , , , , (, ..).
. , .
2:
️2.1 :
. 10 , . . , 10 (, , ), , ,
? ?
: 2019- , TDD , , , . , , ,
. IoT-, Kafka RabbitMQ, , - . , , ? (, , ), , - .
( ) , , (« API, , !» (consumer-driven contracts)). , : , , , .
: TDD - . TDD , . , .
. ROI, Fuzz, , 10 .
. Cindy Sridharan 'Testing Microservices — the sane way'

Example:
2.2
. , . , . , , ? — . : TDD-, .
«», API, , (, , in-memory ), , , . , , « », .
. , , 20 %.
. Express API ( ).

2.3 , API
. , ( ). - , ! — , , . «
-22 » : , , .
(consumer-driven contracts) PACT : , … ! PACT — «», . PACT- — . , API CI, .
. — .
.
2.4
. , Express-. . , , , JS- {req,res}. , (,
Sinon ) {req,res}, , .
node-mock-http {req,res} . , , HTTP-, res- (. ).
. Express- === .
2.5
. . CI- , . (, ), (, ), .
Sonarqube (2600+
)
Code Climate (1500+
). ::
Keith Holliday. , .
. CodeClimate, :

2.6 , Node
. , . ( ) . , - , ? ? , API 50 % ? , Netflix - (
Chaos Engineering ). : , . , Netflix,
chaos monkey , , , , - ( Kubernetes
kube-monkey , ). , . , , Node- , , v8 1,7 , UX , ?
node-chaos (-), , Node.
. , production .
. Node-chaos , Node.js, .

2.7 ,
. ( 0), , , . , (seeds) (
« » ) . , (. « »), , . . , , (, ).
. , , , ? , , , .
. - .
before(() => {
. , .
it("When updating site name, get successful confirmation", async () => {
3:
3.1. UI
. , , , . , , ( HTML CSS) . , (, , , ), , , .
. 10 , 500 (100 = 1 ) - - .
. .
test('When users-list is flagged to show only VIP, should display only VIP members', () => {
. UI . test('When flagging to show only VIP, should display only VIP members', () => {
3.2 HTML- ,
. HTML- , . , , CSS-. , 'test-id-submit-button'. . , , .
. , , . — , , Ajax . . , CSS 'thick-border' 'thin-border'
. , .
. CSS-.
<!-- the markup code (part of React component) --> <span id="metric" className="d-flex-column">{value}</span> <!-- what if the designer changes the classs? -->
3.3
. , , . , , . , — - , (.
« » ). (, ) , .
, : , . ( ) . , .
. , . ?
. .
class Calendar extends React.Component { static defaultProps = {showFilters: false} render() { return ( <div> A filters panel with a button to hide/show filters <FiltersPanel showFilter={showFilters} title='Choose Filters'/> </div> ) } } //Examples use React & Enzyme test('Realistic approach: When clicked to show filters, filters are displayed', () => { // Arrange const wrapper = mount(<Calendar showFilters={false} />) // Act wrapper.find('button').simulate('click'); // Assert expect(wrapper.text().includes('Choose Filter')); // This is how the user will approach this element: by text })
. .
test('Shallow/mocked approach: When clicked to show filters, filters are displayed', () => {
3.4 .
. (, ). (,
setTimeOut
) , . (,
Cypress cy.request('url') ), API,
wait(expect(element)) @testing-library/DOM . , API, , . , ,
hurry-up the clock . — , , ( ). , , - npm- , ,
wait-for-expect .
. , . , . .
. E2E API (Cypress).
. , DOM- (@testing-library/dom).
. .
test('movie title appears', async () => {
3.5.
. - , . , , . :
pingdom , AWS CloudWatch
gcp StackDriver , , SLA. , , (,
lighthouse ,
pagespeed ), . — , :
,
(TTI) . , , , , , DOM, SSL . , CI, 247 CDN.
. , , , , - CDN.
. Lighthouse .

3.6 API
. ( 2), , , ( ). API (,
Sinon ,
Test doubles ), API. . API , ( ). API, . , , API . , : .
. , API 100 , 20 .
3.7 ,
. E2E (end-to-end, ) UI (. 3.6). , , . , , - . , — (, ), . - , , UI-
Cypress Pupeteer . , : 50 , , . 10 . , , , — . , .
. UI , , ( , UI) .
3.8
. , API , , . (before-all), - . , : . , . - API- . , . (, ), , , . , : , API (. 3.6).
. , 200 , 100 , 20 .
. (before-all), (before-each) (, Cypress).
Cypress .
let authenticationToken;
3.9 smoke-,
. production- , , . , , , , . smoke- . production, , , . , smoke- , .
. , , production . /Payment.
. Smoke- .
it('When doing smoke testing over all page, should load them all successfully', () => {
3.10
. , . «» , , , , . , ( ) , , -, , , . « », .
. ,
Cucumber JavaScript .
StoryBook UI- , (, , , ..) , . , , .
. , .
. cucumber-js.
. Storybook , .

3.11
. , . , . , . , - . , . , , , . , - . UI « ». , (,
wraith , PhantomCSS), . (,
Applitools ,
Perci.io ) , , « » (, ), DOM/CSS, .
. , ( ) , ?
. : , .

. wraith UI.
# Add as many domains as necessary. Key will act as a label domains: english: "http://www.mysite.com" # Type screen widths below, here are a couple of examples screen_widths: - 600 - 768 - 1024 - 1280 # Type page URL paths below, here are a couple of examples paths: about: path: /about selector: '.about' subscribe: selector: '.subscribe' path: /subscribe
4:
4.1 (~80 %),
. — , . , . — (, ), . ? , 10-30 % . 100 % , . . , : Airbus, ; , 50 % . , , 80 % (
Fowler: «in the upper 80s or 90s» ), , , .
: (CI), (
Jest ) , . , . , ( ) — . , — , , . , .
. . , , . .
.
. ( Jest).

4.2 ,
. , . , , , , . , , - , .
PricingCalculator
, , , 10 000 … , , . , . 80- , . : , , , . , - .
. , , , .
. ? , QA . : , - . , - API .

4.3
. : 100 %, . ? , , , . . - : , , , .
, . JavaScript-
Stryker :
- « ». ,
newOrder.price===0
newOrder.price!=0
. «» .
- , , : , . , , .
, , , .
. , 85- 85 % .
. 100 %, 0 %.
function addNewOrder(newOrder) { logger.log(`Adding new order ${newOrder}`); DB.save(newOrder); Mailer.sendMail(newOrder.assignee, `A new order was places ${newOrder}`); return {approved: true}; } it("Test addNewOrder, don't use such test names", () => { addNewOrder({asignee: "John@mailer.com",price: 120}); });
. Stryker reports, , ().

4.4 -
. ESLint. ,
eslint-plugin-mocha , (
describe()
),
, .
eslint-plugin-jest , ( ).
. 90- , , , . , .
. , , .
describe("Too short description", () => { const userToken = userService.getDefaultToken()
5: CI
5.1 ,
. — . , . , ( !). , . (
ESLint standard Airbnb ), . ,
eslint-plugin-chai-expect , .
Eslint-plugin-promise ( ).
Eslint-plugin-security , DOS-.
eslint-plugin-you-dont-need-lodash-underscore , , V8, ,
Lodash._map(…)
.
. , , . What's happening? , , . . , , .
. , . , ESLint production-.

5.2
. CI , , ..? ,
. What for? : (1) -> (2) -> (3) . , , .
, , , , - .
CI- ( ,
CircleCI local CLI ) . ,
wallaby , ( ) . npm- package.json, , (, , , ). (non-zero exit code)
concurrently . — ,
npm run quality
. githook (
husky ).
. , .
. Npm-, , , .
"scripts": { "inspect:sanity-testing": "mocha **/**--test.js --grep \"sanity\"", "inspect:lint": "eslint .", "inspect:vulnerabilities": "npm audit", "inspect:license": "license-checker --failOn GPLv2", "inspect:complexity": "plato .", "inspect:all": "concurrently -c \"bgBlue.bold,bgMagenta.bold,yellow\" \"npm:inspect:quick-testing\" \"npm:inspect:lint\" \"npm:inspect:vulnerabilities\" \"npm:inspect:license\"" }, "husky": { "hooks": { "precommit": "npm run inspect:all", "prepush": "npm run inspect:all" } }
5.3 production-
. — CI-. . —
Docker-compose . (, ) production-.
AWS Local AWS-.
, serverless
AWS SAM Faas-.
Kubernetes CI-, . , « Kubernetes»
Minikube MicroK8s , , . « Kubernetes»: CI- (,
Codefresh ) Kubernetes-, CI- ; .
. .
: CI-, Kubernetes-
(Dynamic-environments Kubernetes )
deploy: stage: deploy image: registry.gitlab.com/gitlab-examples/kubernetes-deploy script: - ./configureCluster.sh $KUBE_CA_PEM_FILE $KUBE_URL $KUBE_TOKEN - kubectl create ns $NAMESPACE - kubectl create secret -n $NAMESPACE docker-registry gitlab-registry --docker-server="$CI_REGISTRY" --docker-username="$CI_REGISTRY_USER" --docker-password="$CI_REGISTRY_PASSWORD" --docker-email="$GITLAB_USER_EMAIL" - mkdir .generated - echo "$CI_BUILD_REF_NAME-$CI_BUILD_REF" - sed -e "s/TAG/$CI_BUILD_REF_NAME-$CI_BUILD_REF/g" templates/deals.yaml | tee ".generated/deals.yaml" - kubectl apply --namespace $NAMESPACE -f .generated/deals.yaml - kubectl apply --namespace $NAMESPACE -f templates/my-sock-shop.yaml environment: name: test-for-ci
5.4
. , , . , 500 , , . , CI- (
Jest ,
AVA Mocha ) , . CI- (!), . , CLI , , .
. — , .
5.5
. , . 10 ? CI- npm-
license check plagiarism check ( ), , , Stackoveflow .
. , , .
.

5.6
. , Express, .
npm audit ,
snyk ( ). CI .
. . .
: NPM Audit

5.7
. package-lock.json Yarn npm ( ): .
npm install
npm update
, . , — . , package.json
ncu .
, . :
- CI , , npm outdated npm-check-updates (ncu). .
- , pull request' .
: ? , ( ,
eslint-scope ). « »:
latest , , (, 1.3.1, — 1.3.8).
. , .
5.8 CI-, Node
. , Node . , Node.
- . , Jenkins .
- Docker.
- , . . smoke-, (, , ) .
- , , , , , .
- , . , -. - ( ).
- . .
- , , .
- (, Docker-).
- , .
node_modules
.
. , .
5.9 : CI-, Node
. , , . Node, CI . , MySQL, Postgres. CI- «», MySQl, Postgres Node. , - (, ). CI, , .
. - ?
: Travis ( CI) Node.
language: node_js node_js: - "7" - "6" - "5" - "4" install: - npm install script: - npm run test