By the end of this chapter, you will be able to:
In this chapter, we will focus on improving code quality, setting up tests, and automating tests to run before a Git commit. These techniques can be used to ensure that mistakes or errors are found early on and never make it to production.
In the previous chapter, we explored the concepts of modular design, ES6 modules, and their use with Node.js. We took our compiled ES6 JavaScript and converted it into a compatible script using Babel.
In this chapter, we'll discuss code quality, which is one of the key qualities of professional JavaScript development. When we start writing code, we tend to focus on solving simple problems and evaluating the outcome. When it comes to the small pet projects that most developers start with, there is little need to communicate with others or work as part of a large team.
As the projects, you work on becoming larger in scope, the importance of code quality increases. In addition to ensuring that the code works, we have to consider other developers who will use the components we create or update the code we write.
There are several aspects of quality code. The first and most obvious is that it does what it is intended to do. This is often easier said than done. Often, it can be difficult to meet the requirements of a large project. To make matters more complex, often adding a new feature can cause an error in some existing part of the application. We can reduce these mistakes through good design but, even so, these types of breakages are bound to happen.
As agile development becomes more popular, the speed at which code changes have also increased. As a result, tests are more important than ever. We'll demonstrate how you can use unit tests to confirm the proper functioning of functions and classes. In addition to unit tests, we'll look at integration testing, which ensures that all aspects of the program function together correctly as expected.
The second component of code quality is performance. The algorithms in our code may produce the desired result, but do they do so efficiently? We'll look at how you can test functions for performance to ensure that algorithms can return results in an acceptable amount of time when processing a large input. As an example, you may have a sorting algorithm that works great with 10 rows of data but takes several minutes once you try processing 100.
The third aspect of code quality we'll talk about in this chapter is readability. Readability is a measure of how easy it is for a human to read and understand your code. Have you ever looked at code written with vague functions and variable names or variable names that are misleading? When writing code, consider that others may have to read or modify it. Following some basic guidelines can help to improve your readability.
One of the easiest ways to make code more readable is clear naming. Make using variables and functions as obvious as possible. Even on a one-man project, it's easy to come back to your own code after 6 months and have trouble remembering what every function does. When you're reading someone else's code, this is doubly true.
Make sure your names are clear and pronounceable. Consider the following example, where a developer has created a function that returns the date in yymm format:
function yymm() {
let date = new Date();
Return date.getFullYear() + "/" + date.getMonth();
}
When we're given the context and explanation of what this function does, it's obvious. But for an outside developer skimming over the code for the first time, yymm can easily cause some confusion.
Vague functions should be renamed in a way that makes their use obvious:
function getYearAndMonth() {
let date = new Date();
return date.getFullYear() + "/" + date.getMonth();
}
When the correct naming of functions and variables is used, it becomes easy to compose code that is easily readable. Consider another example, in which we want to turn on a light if it's nighttime:
if(time>1600 || time<600) {
light.state = true;
}
It's not at all clear what's going on in the preceding code. What exactly is meant by 1600 and 600, and what does it mean if the light's state is true? Now consider the same function rewritten as follows:
if(time.isNight) {
light.turnOn;
}
The preceding code makes the same process clear. Instead of asking whether the time is between 600 and 1600, we simply ask whether it is night, and, if so, we turn the light on.
In addition to being more readable, we have also put the definition of when it is nighttime into a central location, isNight. If we want to make night end at 5:00 instead of 6:00, we only have to change a single line within isNight instead of finding all instances of time<600 in our code.
When it comes to the convention of how to format or write code, there are two categories: industry- or language-wide convention and company/organization-wide convention. The industry- or language-specific conventions are generally accepted by most programmers using a language. For example, in JavaScript, an industry-wide convention is the use of camel case for variable names.
Good sources for industry-wide conventions include W3 JavaScript Style Guide and Mozilla MDN Web Docs.
In addition to industry-wide conventions, software development teams or projects will often have a further set of conventions. Sometimes, these conventions are compiled into a style guide document; in other cases, these conventions are undocumented.
If you're part of a team that has a relatively large code base, documenting the specific style choices can be useful. This will help you to consider what aspects you'd like to keep and enforce new updates, and which aspects you may want to change. It also helps onboarding new employees who may be familiar with JavaScript but not familiar with the specifics of the company.
A good example of a company-specific style guide is Google JavaScript Style Guide (https://google.github.io/styleguide/jsguide.html). It contains some information that is useful in general. For example, Section 2.3.3 discusses the use of non-ASCII in code. It suggests the following:
const units = 'μs';
Is preferable to using something like:
const units = 'u03bcs'; // 'μs'
Using u03bcs without the comment would be even worse. The more obvious the meaning of your code, the better.
Companies often have a set of libraries they favor for doing things such as logging, working with time values (for example, the Moment.js library), and testing. This can be useful for compatibility and the reuse of code. Having multiple dependencies that do similar things, used by different developers, increases the size of the compiled project, for example, if a project is already using Bunyan for logging, and someone else decides to install an alternative library such as Morgan.
It's worth taking the time to read over some of the more popular style guides for JavaScript. Don't feel obligated to follow every single rule or suggestion, but get accustomed to the thinking behind why rules are created and enforced. Some popular guides worth checking out include the following:
MSDN Style Guide: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide
When it comes to convention, the term "opinionated" is one you will likely come across. When exploring existing libraries and frameworks, you will often see phrases such as "an opinionated framework." In this context, "opinionated" is a measure of how strictly a convention is enforced:
Opinionated: Strictly enforces its chosen conventions and methods of doing things
Non-opinionated: Does not enforce convention, that is, as long as the code is valid, it can be used
Linting is an automated process where code is examined and validated against a standard of style guidelines. For example, a project that has linting set up to ensure two spaces instead of tabs will detect instances of tabs and prompt the developer to make the change.
It's important to be aware of linting, but it's not a strict requirement for your projects. When I'm working on a project, the main points I consider when deciding whether linting is needed are the size of the project and the size of the team working on the project.
Linting really comes in handy on long-term projects with medium- to large-sized teams. Often, new people join the project with experience of using some other styling convention. This means that you start getting mixed styles between files or even within the same file. This leads to the project becoming less organized and harder to read.
If you're writing a prototype for a hackathon, on the other hand, I would suggest that you skip the linting. It adds overhead to a project, that is unless you're using a boilerplate project as your starting point, which comes with your preferred linting installed.
There is also the risk of linting systems that are too restrictive and end up slowing down development.
Good linting should consider the project and find a balance between enforcing a common style and not being too restrictive.
In this exercise, we will install and set up ESLint and Prettier to monitor our code for styling and syntax errors. We will use a popular ESLint convention that was developed by Airbnb and has become somewhat of a standard.
The code files for this exercise can be found at https://github.com/TrainingByPackt/Professional-JavaScript/tree/master/Lesson06/Exercise29/result.
Perform the following steps to complete the exercise:
mkdir Exercise29
cd Exercise29
npm init -y
npm install --save-dev eslint prettier eslint-config-airbnb-base eslint-config-prettier eslint-plugin-jest eslint-plugin-import
We're installing several developer dependencies here. In addition to eslint and prettier, we're also installing a starting config made by Airbnb, a config to work with Prettier, and an extension that adds style exceptions for our Jest-based test files.
{
"extends": ["airbnb-base", "prettier"],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"env": {
"browser": true,
"node": true,
"es6": true,
"mocha": true,
"jest": true,
},
"plugins": [],
"rules": {
"no-unused-vars": [
"error",
{
"vars": "local",
"args": "none"
}
],
"no-plusplus": "off",
}
}
node_modules
build
dist
var square = x => x * x;
console.log(square(5));
"scripts": {
"lint": "prettier --write src/**/*.js"
},
npm run lint
"scripts": {
"lint": "prettier --write src/**/*.js && eslint src/*.js"
},
> prettier --write src/**/*.js && eslint src/*.js
src/square.js 49ms
/home/philip/packt/lesson_6/lint/src/square.js
1:1 error Unexpected var, use let or const instead no-var
2:1 warning Unexpected console statement no-console
2 problems (1 error, 1 warning)
1 error and 0 warnings potentially fixable with the --fix option.
The preceding script produces one error and one warning. The error is due to the use of var when let or const could be used. Although, in this particular case, const should be used, as the value of square is not reassigned. The warning is in regard to our use of console.log, which generally shouldn't be shipped in production code, as it will make it hard to debug the console output when an error occurs.
> prettier --write src/**/*.js && eslint src/*.js
src/js.js 48ms
/home/philip/packt/lesson_6/lint/src/js.js
2:1 warning Unexpected console statement no-console
1 problem (0 errors, 1 warning)
In this exercise, we installed and set up Prettier for automatic code formatting, and ESLint to check our code for common bad practices.
A unit test is an automated software test that checks whether a single aspect or function in some software is working as expected. For example, a calculator application might be split up into functions that deal with the Graphical User Interface (GUI) of the application and another set of functions responsible for each type of mathematical calculation.
In such a calculator, unit tests might be set up to ensure that each mathematical function works as expected. This setup allows us to quickly find any inconsistent results or broken functions caused by any changes. As an example, such a calculator's test file might include the following:
test('Check that 5 plus 7 is 12', () => {
expect(math.add(5, 7)).toBe(12);
});
test('Check that 10 minus 3 is 7', () => {
expect(math.subtract(10, 3)).toBe(7);
});
test('Check that 5 multiplied by 3 is 15', () => {
expect(math.multiply(5, 3).toBe(15);
});
test('Check that 100 divided by 5 is 20', () => {
expect(math.multiply(100, 5).toBe(20);
});
test('Check that square of 5 is 25', () => {
expect(math.square(5)).toBe(25);
});
The preceding tests would run every time the code base was changed and be checked into version control. Often, errors will arise unexpectedly when a function that is used in multiple places is updated and causes a chain reaction, breaking some other function. If such a change happens and one of the preceding statements becomes false (for example, 5 multiplied by 3 returns 16 instead of 15), we will immediately be able to associate our new code change with the break.
This is a very powerful technique that can be taken for granted in environments where tests are already set up. In work environments without such a system, it's possible that changes from developers or updates in software dependencies that unexpectedly break an existing function are committed to source control. Later, the bug is found, and it becomes difficult to make the association between the broken function and the code change that caused it.
It's also important to remember that unit tests ensure the functionality of some sub-unit of work, but not the functionality of a project as a whole (where multiple functions work together to produce a result). This is where integration testing comes into play. We will explore integration tests later on within this chapter.
In this exercise, we will demonstrate setting up a unit test using Jest, the most popular testing framework in the JavaScript ecosystem. We will continue with our example of a calculator application and set up automated testing for a function that takes a number and outputs its square.
The code files for this exercise can be found at https://github.com/TrainingByPackt/Professional-JavaScript/tree/master/Lesson06/Exercise30.
Perform the following steps to complete the exercise:
npm init -y
npm install --save-dev jest
mkdir __tests__
const math = require('./../src/math.js');
test('Check that square of 5 is 25', () => {
expect(math.square(5)).toBe(25);
});
FAIL __test__/math.test.js
✕ Check that square of 5 is 25 (17ms)
● Check that square of 5 is 25
expect(received).toBe(expected) // Object.is equality
Expected: 25
Received: 10
2 |
3 | test('Check that square of 5 is 25', () => {
> 4 | expect(math.square(5)).toBe(25);
| ^
5 | });
6 |
at Object.toBe (__test__/math.test.js:4:26)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 1.263s
This error triggers because the start code has purposely included an error in the square function. Instead of multiplying the number by itself, we have instead doubled the value. Notice that the number of received answers was 10.
const square = (x) => x * x;
In this exercise, we set up a Jest test to ensure that running our square function with an input of 5 returns 25. We also looked at what to expect when the wrong value is returned by running our test with a mistake in the code that returned 10 instead of 25.
So, we have discussed unit tests, which are extremely useful for finding the cause of errors when a project's code changes. However, it's also possible that the project passes all unit tests yet does not work as expected. This is because the whole of the project contains additional logic that glues our functions together, as well as static components such as HTML, data, and other artifacts.
Integration tests can be used to ensure a project works from a higher level. For example, while our unit tests directly call functions such as math.square, an integration test will test multiple pieces of functionality working together for a particular result.
Often, this means bringing together multiple modules or interacting with a database or other external components or APIs. Of course, integrating more parts means integration tests take longer, so they should be used more sparingly than unit tests. Another downside of the integration test is that when one fails, there are multiple possibilities as to the cause. In contrast, a failed unit test is generally easy to fix as the code being tested is in a specified location.
In this exercise, we'll continue where we left off in our last Jest exercise, where we tested that the square function was returning 25 in response to 5. In this exercise, we'll continue by adding some new tests that use our functions in conjunction with each other:
npm install
mkdir __tests__
const math = require('./../src/math.js');
test('check that square of result from 1 + 1 is 4', () => {
expect(math.square(math.add(1,1))).toBe(4);
});
test('check that square of result from 1 + 1 is 4', () => {
const start = new Date();
expect(math.square(math.add(1,1))).toBe(4);
expect(new Date() - start).toBeLessThan(5000);
});
You should see an output similar to the preceding figure, with each test passing with an expected result.
It should be noted that these integration tests are somewhat simplistic. In a real-world scenario, integration tests combine functions, as we demonstrated previously, but from different sources. For example, when you have multiple components created by different teams, integration testing is there to ensure that everything works together. Often, bugs can be caused by simple things, such as updating an external library.
The idea is that multiple parts of your application are integrated, giving you a greater chance of finding where something breaks.
Often, a problem has more than one solution. While all solutions might return the same result, they likely don't have the same performance. Take, for example, the problem of getting the nth number of the Fibonacci sequence. Fibonacci is a mathematical pattern where the next number in the sequence is the sum of the last two numbers (1, 1, 2, 3, 5, 8, 13, …).
Consider the following solution, where Fibonacci calls itself recursively:
function fib(n) {
return (n<=1) ? n : fib(n - 1) + fib(n - 2);
}
The preceding example states that if we want to get the nth number of the Fibonacci sequence recursively, then get the Fibonacci of n minus one plus the Fibonacci of n minus two, unless n is 1, in which case, return 1. It works and will return the correct answer for any given number. However, as the value of n increases, the execution time increases exponentially.
To see how slow this performs, add the fib function to a new file and use the function by console logging the result as follows:
console.log(fib(37));
Next, on the command line, run the following command (time should be available in most Unix- and Mac-based environments):
time node test.js
On a particular laptop, I got back the following, which indicates the 37th digit of Fibonacci is 24157817 and the execution time took 0.441 seconds:
24157817
real 0m0.441s
user 0m0.438s
sys 0m0.004s
Now open up that same file and change 37 to 44. Then, run the same time node test command again. In my case, an increase of only 7 caused the execution time to increase up to 20 times:
701408733
real 0m10.664s
user 0m10.653s
sys 0m0.012s
We can rewrite the same algorithm in a more efficient way to increase speed for larger numbers:
function fibonacciIterator(a, b, n) {
return n === 0 ? b : fibonacciIterator((a+b), a, (n-1));
}
function fibonacci(n) {
return fibonacciIterator(1, 0, n);
}
Even though it appears to be more complex, this method of generating a Fibonacci number is superior due to the speed of execution.
One of the downsides of tests with Jest is that, given the preceding scenario, both the slow and fast versions of Fibonacci will pass. Yet, the slow version would clearly be unacceptable in a real-world application where quick processing has to be done.
To guard against this, you may want to add some performance-based tests that ensure functions are completed within a certain time period. The following is an example of creating a custom timer to ensure that a function finishes within 5 seconds:
test('Timer - Slow way of getting Fibonacci of 44', () => {
const start = new Date();
expect(fastFib(44)).toBe(701408733);
expect(new Date() - start).toBeLessThan(5000);
});
It can be somewhat cumbersome to manually add timers to all your functions. For this reason, there is discussion within the Jest project to create an easier syntax for accomplishing what has been done previously.
To see the discussion related to this syntax and whether it's been resolved, check issue #6947 for Jest on GitHub at https://github.com/facebook/jest/issues/6947.
In this exercise, we'll use the technique described previously to test the performance of two algorithms for getting Fibonacci:
npm install
mkdir __tests__
const fastFib = require('./../fastFib');
const slowFib = require('./../slowFib');
test('Fast way of getting Fibonacci of 44', () => {
const start = new Date();
expect(fastFib(44)).toBe(701408733);
expect(new Date() - start).toBeLessThan(5000);
});
test('Timer - Slow way of getting Fibonacci of 44', () => {
const start = new Date();
expect(slowFib(44)).toBe(701408733);
expect(new Date() - start).toBeLessThan(5000);
});
Notice the preceding error response for the part that mentions the timer. The expected result for the function's running time was under 5,000 milliseconds but, in my case, I actually received 10,961. You'll likely get a different result based on the speed of your computer. If you didn't receive the error, it may be that your computer is so fast that it completed in less than 5,000 milliseconds. If that's the case, try lowering the expected maximum time to trigger the error.
While integration testing combines multiple units or functions of a software project, end-to-end testing goes one step further by simulating the actual use of the software.
For example, while our unit tests directly called functions such as math.square, an end-to-end test would load the graphical interface of the calculator and simulate pressing a number, say 5, followed by the square button. After a few seconds, the end-to-end test would look at the resulting answer in the graphical interface and ensure it equals 25 as expected.
End-to-end testing should be used more sparingly due to the overhead, but it is a great final step in a testing process to ensure that everything is working as expected. In contrast, unit tests are relatively quick to run and, therefore, can be run more often without slowing down development. The following figure shows a recommended distribution of tests:
It should be noted that there can be some overlap between what is considered an integration test and what is considered an end-to-end test. The interpretation of what constitutes a test type may vary between one company and another.
Traditionally, tests have been classified as either a unit test or an integration test. Over time, other classifications have become popular, such as system, acceptance, and end-to-end. Due to this, there can be an overlap as to what type a particular test is.
In 2018, Google released the Puppeteer JavaScript library, which has drastically increased the ease with which end-to-end testing can be set up on a JavaScript-based project. Puppeteer is a headless version of the Chrome web browser, meaning that it has no GUI component. This is crucial, as it means we're testing our applications with a full Chrome browser, rather than a simulation.
Puppeteer can be controlled through jQuery-like syntax, where elements on an HTML page are selected by ID or class and interacted with. For example, the following code opens Google News, finds a .rdp59b class, clicks on it, waits 3 seconds, and finally takes a screenshot:
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://news.google.com');
const more = await page.$(".rdp59b");
more.click();
await page.waitFor(3000);
await page.screenshot({path: 'news.png'});
await browser.close();
})();
Bear in mind that, in the preceding example, we're selecting a .rdp59b class that looks like it was automatically generated; therefore, it is likely that this class will change in the future. In the case that the class name changes, the script will no longer work.
If upon reading this, you find the preceding script does not work, I challenge you to update it. One of the best tools when working with Puppeteer is the Chrome DevTools. My usual workflow is to go to the website I'm writing a script for and right-click on the element that I'll be targeting, as shown in the following figure:
Once you click on Inspect, the DOM explorer will pop up and you'll be able to see any classes or IDs associated with the element:
In addition to being useful for writing end-to-end tests, Puppeteer can also be used for web scraping and automation. Almost anything that can be done in a normal browser can be automated (given the right code).
In addition to being able to select elements on a page via selectors, as we previously looked at, Puppeteer has full access to keyboard and mouse simulation. Thus, more complex things such as automating web-based games and daily tasks are possible. Some have even managed to bypass things such as captchas using it.
In this exercise, we're going to use Puppeteer to manually open an HTML/JavaScript-based calculator and use it as an end user would. I didn't want to target a live website as its content often changes or goes offline. So, instead, I have included an HTML calculator in Exercise33/start of the project files.
You can view it by installing dependencies with npm, running npm start, and then going to localhost:8080 in your browser:
In this exercise, we'll be creating a script that opens the site, presses the buttons, and then checks the site for the correct results. Instead of just checking the output of a function, we're listing actions to take on the site and specifying the HTML selector to use as the value to run our tests against.
Perform the following steps to complete the exercise:
npm install
npm install --save-dev jest puppeteer jest-puppeteer
"jest": {
"preset": "jest-puppeteer"
},
module.exports = {
server: {
command: 'npm start',
port: 8080,
},
}
The preceding configuration will make sure the npm start command is run before the testing phase. It also tells Puppeteer to look for our web application on port: 8080.
mkdir __test__
describe('Calculator', () => {
beforeAll(async () => {
await page.goto('http://localhost:8080')
})
it('Check that 5 times 5 is 25', async () => {
const five = await page.$("#five");
const multiply = await page.$("#multiply");
const equals = await page.$("#equals");
await five.click();
await multiply.click();
await five.click();
await equals.click();
const result = await page.$eval('#screen', e => e.innerText);
expect(result).toMatch('25');
})
})
The preceding code is a complete end-to-end test for multiplying 5 by 5 and confirming that the answer returned within the interface is 25. Here, we're opening the local website, pressing five, pressing multiply, pressing five, pressing equals, and then checking the value of the div with the ID of screen.
You should see a result, as shown in the preceding figure, with the output of 25.
The tests and linting commands discussed here can be incredibly useful for maintaining and improving your code quality and functionality. However, in the heat of actual development, where our focus is on specific problems and deadlines, it can be easy to forget to run the linting and test commands.
One popular solution to this problem is the use of Git hooks. A Git hook is a feature of the Git version control system. A Git hook specifies a terminal command to be run at some specific point in the Git process. A Git hook can be run before a commit; after, when a user updates by pulling; and at many other specific points. A full list of possible Git hooks can be found at https://git-scm.com/docs/githooks.
For our purposes, we'll focus only be using the pre-commit hook. This will allow us to find any formatting issues before we commit our code to the source.
Another interesting way of exploring the possible Git hooks and how they're used in general is to open any Git version control project and look in the hooks folder.
By default, any new .git project will contain a large list of samples in the .git/hooks folder. Explore their contents and have them trigger by renaming them with the following pattern:
<hook-name>.sample to <hook-name>
In this exercise, we'll set up a local Git hook that runs the lint command before we're allowed to commit using Git:
npm install
git init
#!/bin/sh
npm run lint
chmod +x .git/hooks/pre-commit
git add package.json
git commit -m "testing git hook"
The following is the output of the preceding code:
You should see the lint command being run before your code is committed to the source, as shown in the preceding screenshot.
let number = square(5);
Make sure that you keep the unnecessary tab in the preceding line, as this will be what triggers a lint error.
git add src/js.js
git commit -m "testing bad lint"
The following is the output of the preceding code:
You should see the lint command running as before; however, after it runs, the code is not committed like the last time, due to the Git hook returning an error.
An important factor to be aware of with Git hooks is that, because these hooks are within the .git folder itself, they are not considered part of the project. Therefore, they will not be shared to your central Git repository for collaborators to use.
However, Git hooks are most useful in collaborative projects where new developers may not be fully aware of a project's conventions. It's a very convenient process when a new developer clones a project, makes some changes, tries to commit, and immediately gets feedback based on linting and tests.
The husky node library was created with this in mind. It allows you to keep track of your Git hooks within the source code using a single config file called .huskyrc. When a project is installed by a new developer, the hooks will be active without the developer having to do anything.
In this exercise, we're going to set up a Git hook that does the same thing as the one in Exercise 34, Setting up a Local Git Hook, but has the advantage of being shareable across a team. By using the husky library instead of git directly, we'll ensure that anyone who clones the project also has the hook that runs lint before committing any changes:
npm install
{
"hooks": {
"pre-commit": "npm run lint"
}
}
The preceding file is the most important part of this exercise as it defines exactly what command will be run at what point in the Git process. In our case, we're running the lint command before any code is committed to the source.
git init
npm install --save-dev husky
git add src/js.js
git commit -m "test husky hook"
The following is the output of the preceding code:
We're getting a warning about our use of console.log, but you can ignore this for our purposes. The main point is that we have set up our Git hook using Husky, so anyone else who installs the project will also have the hooks set up, as opposed to if we set them up directly in Git.
Take note of the fact that npm install --save-dev husky was run after our Git repository was created. When you install Husky, it runs the required commands to set up your Git hooks. However, if the project isn't a Git repository, it won't be able to.
If you have any problems related to this, try re-running npm install --save-dev husky once you have initialized a Git repository.
In this exercise, we're going to write a Puppeteer test that verifies a small quiz app is working. If you go into the exercise folders and find the starting point for Exercise 36, you can run npm start to see the quiz we'll be testing:
In this application, clicking on the correct answer of a question makes the question disappear and the score increment by one:
npm install --save-dev jest puppeteer jest-puppeteer
"scripts": {
"start": "http-server",
"test": "jest"
},
"jest": {
"preset": "jest-puppeteer"
},
module.exports = {
server: {
command: 'npm start',
port: 8080,
},
}
mkdir __test__
describe('Quiz', () => {
beforeAll(async () => {
await page.goto('http://localhost:8080')
})
// tests will go here
})
it('Check question #1', async () => {
const q1 = await page.$("#q1");
let rightAnswer = await q1.$x("//button[contains(text(), '10')]");
await rightAnswer[0].click();
const result = await page.$eval('#score', e => e.innerText);
expect(result).toMatch('1');
})
Notice our use of q1.$x("//button[contains(text(), '10')]"). Instead of using an ID, we're searching the answers for a button that contains the text 10. This can be very useful when parsing a website that doesn't use IDs on the elements you need to interact with.
it('Check question #2', async () => {
const q2 = await page.$("#q2");
let rightAnswer = await q2.$x("//button[contains(text(), '36')]");
await rightAnswer[0].click();
const result = await page.$eval('#score', e => e.innerText);
expect(result).toMatch('2');
})
it('Check question #3', async () => {
const q3 = await page.$("#q3");
let rightAnswer = await q3.$x("//button[contains(text(), '9')]");
await rightAnswer[0].click();
const result = await page.$eval('#score', e => e.innerText);
expect(result).toMatch('3');
})
it('Check question #4', async () => {
const q4 = await page.$("#q4");
let rightAnswer = await q4.$x("//button[contains(text(), '99')]");
await rightAnswer[0].click();
const result = await page.$eval('#score', e => e.innerText);
expect(result).toMatch('4');
})
Notice how the line at the bottom of each test has an expected result, one higher than the last; this is us tracking the score on the page. If everything is working properly, the fourth test will find a score of 4.
npm test
The following is the output of the preceding code:
If you have done everything correctly, you should see four passing tests as a response to running npm test.
In this activity, we'll combine several aspects of the chapter. Starting with a pre-built calculator using HTML/JavaScript, your task is the following:
Perform the following steps to complete the activity (high-level steps):
Expected Output
After completing the assignment, you should be able to run the npm run lint command and the npm test command and get tests passing like in the preceding screenshot.
The solution for this activity can be found on page 602.
In this chapter, we looked at aspects of code quality with an emphasis on automated testing. We started with the basics of clear naming and getting familiar with the industry-wide conventions of the language. By following these conventions and writing clearly, we're able to make our code more readable and reusable.
Building from there, we looked at how linting and testing commands can be created with Node.js using a handful of popular tools, including Prettier, ESLint, Jest, Puppeteer, and Husky.
In addition to setting up tests, we talked about the categories of tests and their use cases. We went through unit tests that ensure that individual functions are working as expected, and integration tests that combine multiple functions or aspects of a program to ensure things are working together. Then, we performed end-to-end tests, which open the application's interface and interact with it as an end-user would.
Finally, we looked at how we can tie it all together by having our linting and testing scripts automatically run with Git hooks.
In the next chapter, we'll look at constructors, promises, and async/await. We'll be using some of these techniques to refactor JavaScript in a modern way that takes advantage of the new features available in ES6.
18.223.160.61