© Adam Freeman 2019
A. FreemanEssential TypeScripthttps://doi.org/10.1007/978-1-4842-4979-6_6

6. Testing and Debugging TypeScript

Adam Freeman1 
(1)
London, UK
 

In this chapter, I continue the theme of TypeScript development tools started in Chapter 5, which introduced the TypeScript compiler. I show you the different ways that TypeScript code can be debugged, demonstrate the use of TypeScript and the linter, and explain how to set up unit testing for TypeScript code.

Preparing for This Chapter

For this chapter, I continue using the tools project created in Chapter 5. No changes are required for this chapter.

Tip

You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/Apress/essential-typescript .

Open a new command prompt and use it to run the command shown in Listing 6-1 in the tools folder to start the compiler in watch mode. using the tsc-watch package installed in Chapter 5.
npm start
Listing 6-1.

Starting the Compiler

The compiler will start, the TypeScript files in the project will be compiled, and the following output will be displayed:
7:04:50 AM - Starting compilation in watch mode...
7:04:52 AM - Found 0 errors. Watching for file changes.
Message: Hello, TypeScript
Total: 600

Debugging TypeScript Code

The TypeScript compiler does a good job of reporting syntax errors or problems with data types, but there will be times when you have code that compiles successfully but doesn’t execute in the way you expected. Using a debugger allows you to inspect the state of the application as it is executing and can reveal why problems occur. In the sections that follow, I show you how to debug a TypeScript application that is executed by Node.js. In Part 3, I show you how to debug TypeScript web applications.

Preparing for Debugging

The difficulty with debugging a TypeScript application is that the code being executed is the product of the compiler, which transforms the TypeScript code into pure JavaScript. To help the debugger correlate the JavaScript code with the TypeScript code, the compiler can generate files known as source maps. Listing 6-2 enables source maps in the tsconfig.json file.
{
    "compilerOptions": {
        "target": "es2018",
        "outDir": "./dist",
        "rootDir": "./src",
        "noEmitOnError": true,
        "module": "commonjs",
        "sourceMap": true
    }
}
Listing 6-2.

Enabling Source Maps in the tsconfig.json File in the tools Folder

When the compiler next compiles the TypeScript files, it will also generate a map file, which has the map file extension, alongside the JavaScript files in the dist folder.

Adding Breakpoints

Code editors that have good TypeScript support, such as Visual Studio Code, allow breakpoints to be added to code files. My experience with this feature has been mixed, and I have found them unreliable, which is why I rely on the less elegant but more predictable debugger JavaScript keyword. When a JavaScript application is executed through a debugger, execution halts when the debugger keyword is encountered, and control is passed to the developer. The advantage of this approach is that it is reliable and universal, but you must remember to remove the debugger keyword before deployment. Most runtimes ignore the debugger keyword during normal execution, but it isn’t a behavior that can be counted on. (Linting, described later in this chapter, can help avoid leaving the debugger keyword in code files.) In Listing 6-3, I have added the debugger keyword to the index.ts file.
import { sum } from "./calc";
let printMessage = (msg: string): void =>  console.log(`Message: ${ msg }`);
let message = ("Hello, TypeScript");
printMessage(message);
debugger;
let total = sum(100, 200, 300);
console.log(`Total: ${total}`);
Listing 6-3.

Adding the debugger Keyword in the index.ts File in the src Folder

There will be no change in the output when the code is executed because Node.js ignores the debugger keyword by default.

Using Visual Studio Code for Debugging

Most good code editors have some degree of support for debugging TypeScript and JavaScript code. In this section, I show you how to perform debugging with Visual Studio Code to give you an idea of the process. There may be different steps required if you use another editor, but the basic approach is likely to be similar.

To set up the configuration for debugging, select Add Configuration from the Debug menu and select Node.js from the list of environments when prompted, as shown in Figure 6-1.

Note

If selecting the Add Configuration menu doesn’t work, try selecting Start Debugging instead.

../images/481342_1_En_6_Chapter/481342_1_En_6_Fig1_HTML.jpg
Figure 6-1.

Selecting the debugger environment

The editor will create a .vscode folder in the project and add to it a file called launch.json, which is used to configure the debugger. Change the value of the program property so that the debugger executes the JavaScript code from the dist folder, as shown in Listing 6-4.
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "program": "${workspaceFolder}/dist/index.js"
        }
    ]
}
Listing 6-4.

Changing the Code Path in the launch.json File in the .vscode Folder

Save the changes to the launch.json file and select Start Debugging from the Debug menu. Visual Studio Code will execute the index.js file in the dist folder under the control of the Node.js debugger. Execution will continue as normal until the debugger statement is reached, at which point execution halts and control is transferred to the debugging pop-up, as shown in Figure 6-2.
../images/481342_1_En_6_Chapter/481342_1_En_6_Fig2_HTML.jpg
Figure 6-2.

Debugging an application using Visual Studio Code

The state of the application is displayed in the sidebar, showing the variables that are set at the point that execution was halted. A standard set of debugging features is available, including setting watches, stepping into and over statements, and resuming execution. The Debug Console window allows JavaScript statements to be executed in the context of the application so that entering a variable name and pressing Return, for example, will return the value assigned to that variable.

Using the Integrated Node.js Debugger

Node.js provides a basic integrated debugger. Open a new command prompt and use it to run the command shown in Listing 6-5 in the tools folder.

Note

There are no hyphens before the inspect argument in Listing 6-5. Using hyphens enables the remote debugger described in the following section.

node inspect dist/index.js
Listing 6-5.

Starting the Node.js Debugger

The debugger starts, loads the index.js file, and halts execution. Enter the command shown in Listing 6-6 and press Return to continue execution.
c
Listing 6-6.

Continuing Execution

The debugger halts again when the debugger statement is reached. You can execute expressions to inspect the state of the applications using the exec command, although expressions have to be quoted as strings. Enter the command shown in Listing 6-7 at the debug prompt.
exec("message")
Listing 6-7.

Evaluating an Expression in the Node.js Debugger

Press Return, and the debugger will display the value of the message variable, producing the following output:
'Hello, TypeScript'

Type help and press Return to see a list of commands. Press Control+C twice to end the debugging session and return to the regular command prompt.

Using the Remote Node.js Debugging Feature

The integrated Node.js debugger is useful but awkward to use. The same features can be used remotely using the Google Chrome developer tools feature. First, start Node.js by running the command shown in Listing 6-8 in the tools folder.
node --inspect-brk dist/index.js
Listing 6-8.

Starting Node.js in Remote Debugger Mode

The inspect-brk argument starts the debugger and halts execution immediately. This is required for the example application because it runs and then exits. For applications that start and then enter an indefinite loop, such as a web server, the inspect argument can be used. When it starts, Node.js will produce a message like this:
Debugger listening on ws://127.0.0.1:9229/e3cf5393-23c8-4393-99a1-d311c585a762
For help, see: https://nodejs.org/en/docs/inspector
The URL in the output is used to connect to the debugger and take control of execution. Open a new Chrome window and navigate to chrome://inspect. Click the Configure button and add the IP address and port from the URL from the previous message. For my machine, this is 127.0.0.1:9229, as shown in Figure 6-3.
../images/481342_1_En_6_Chapter/481342_1_En_6_Fig3_HTML.jpg
Figure 6-3.

Configuring Chrome for remote Node.js debugging

Click the Done button and wait a moment while Chrome locates the Node.js runtime. Once it has been located, it will appear in the Remote Target list, as shown in Figure 6-4.
../images/481342_1_En_6_Chapter/481342_1_En_6_Fig4_HTML.jpg
Figure 6-4.

Discovering the Node.js runtime

Click the “inspect” link to open a new Chrome developer tools window that is connected to the Node.js runtime. Control of execution is handled by the standard developer tool buttons, and resuming execution will let the runtime proceed until the debugger statement is reached. The initial view of the code in the debugger window will be of the JavaScript code, but the source maps will be used once execution resumes, as shown in Figure 6-5.
../images/481342_1_En_6_Chapter/481342_1_En_6_Fig5_HTML.jpg
Figure 6-5.

Debugging with the Chrome developer tools

Using the TypeScript Linter

A linter is a tool that checks code files using a set of rules that describe problems that cause confusion, produce unexpected results, or reduce the readability of the code. The standard linter for TypeScript is TSLint. To add TSLint to the project, use a command prompt to run the command shown in Listing 6-9 in the tools folder.

Note

At the time of writing, the TSLint team has announced they will merge the TSLint functionality into ESLint, which is a popular JavaScript linter. An easy migration path has been promised once ESLint becomes capable of linting TypeScript.

npm install --save-dev [email protected]
Listing 6-9.

Adding a Package to the Example Project

To create the configuration required to use the linter, add a file called tslint.json to the tools folder with the content shown in Listing 6-10.
{
    "extends": ["tslint:recommended"],
    "linterOptions": {
        "format": "verbose"
    }
}
Listing 6-10.

The Contents of the tslint.json File in the tools Folder

The linter comes with preconfigured sets of rules that are specified using the extends setting, as described in Table 6-1.
Table 6-1.

The TSLint Preconfigured Rule Sets

Name

Description

tslint:recommended

This is the set of rules suggested by the TSLint development team and is intended for general TypeScript development.

tslint:latest

This set extends the recommended set to include recently defined rules.

tslint:all

This set contains all of the linter’s rules, which can produce a large number of linting errors.

The linterOptions settings in Listing 6-10 select the verbose output format, which includes the name of the rules in the error messages, which is important when you first start using a linter and need to tailor the linting settings.

Stop the node process using Control+C and run the command shown in Listing 6-11 in the tools folder to run the linter on the example project.
npx tslint --project tsconfig.json --config tslint.json
Listing 6-11.

Running the TypeScript Linter

The project argument tells the linter to use the compiler settings file to locate the source files it will check, although there is only one TypeScript file in the example project. The linter will check the code against the rules in the recommended set and produce the following output:
...
ERROR: (eofline) C:/Users/adam/Documents/Books/Pro TypeScript/Source Code/Current/tools/src/calc.ts[3, 2]: file should end with a newline
ERROR: (prefer-const) C:/Users/adam/Documents/Books/Pro TypeScript/Source Code/Current/tools/src/index.ts[3, 5]: Identifier 'printMessage' is never reassigned; use 'const' instead of 'let'.
ERROR: (no-console) C:/Users/adam/Documents/Books/Pro TypeScript/Source Code/Current/tools/src/index.ts[3, 44]: Calls to 'console.log' are not allowed.
ERROR: (prefer-const) C:/Users/adam/Documents/Books/Pro TypeScript/Source Code/Current/tools/src/index.ts[5, 5]: Identifier 'message' is never reassigned; use 'const' instead of 'let'.
ERROR: (no-debugger) C:/Users/adam/Documents/Books/Pro TypeScript/Source Code/Current/tools/src/index.ts[8, 1]: Use of debugger statements is forbidden
ERROR: (prefer-const) C:/Users/adam/Documents/Books/Pro TypeScript/Source Code/Current/tools/src/index.ts[10, 5]: Identifier 'total' is never reassigned; use 'const' instead of 'let'.
ERROR: (no-console) C:/Users/adam/Documents/Books/Pro TypeScript/Source Code/Current/tools/src/index.ts[11, 1]: Calls to 'console.log' are not allowed.
ERROR: (eofline) C:/Users/adam/Documents/Books/Pro TypeScript/Source Code/Current/tools/src/index.ts[11, 32]: file should end with a newline
...

The linter uses the tsconfig.json file to locate the TypeScript code files and checks them for compliance with the rules in the recommended set. The code in the example project breaks four of the linter’s rules: the eofline rule requires a newline at the end of a code file, the no-debugger rule prevents the debugger keyword from being used, the no-console rule prevents the console object from being used, and the prefer-const keyword requires the const keyword to be used in place of let when the value assigned to a variable isn’t changed.

Disabling Linting Rules

The problem is that the value of a linting rule is often a matter of personal style and preference, and even when the rule is useful, it isn’t always helpful in every situation. Linting works best when you only get warnings that you want to address. If you receive a list of warnings that you don’t care about, then there is a good chance you won’t pay attention when something important is reported.

Of the four rules that are broken by the code in the example project, two of them report issues that I do not consider a problem. I use the console object to write messages as a simple debugging tool, and it is a useful feature for writing book examples. Similarly, I don’t terminate my code files with a new line because many of my code files are used for book examples and a final newline doesn’t fit with the template that I use to write chapters.

The prefer-const rule falls into a different category: it highlights a deficiency in my coding style that I have learned to accept. I know that I should use const instead of let, and that’s what I try to do. But my coding habits are deeply ingrained, and my view is that some problems are not worth fixing, especially since doing so requires breaking my concentration on the larger flow of the code I write. I accept my imperfections and know that I will continue to use let, even when I know that const would be a better choice.

In all three cases, I don’t want the linter to highlight statements that break these rules. Rules that should never be applied to a project can be disabled in the linter configuration file, as shown in Listing 6-12.
{
    "extends": ["tslint:recommended"],
    "linterOptions": {
        "format": "verbose"
    },
    "rules": {
        "eofline": false,
        "no-console": false,
        "prefer-const": false
    }
}
Listing 6-12.

Disabling a Linting Rule in the tslint.json File in the tools Folder

The rules configuration section is populated with the names of the rules and a value of true or false to enable or disable the rules. Some rules can be configured to alter their behavior, such as setting the level of indentation enforced by the linter, for example, but all rules can be switched off for a project using false.

Some rules are useful in a project but disabled for specific files or statements. This is the category into which the no-debugger rule falls. As a general principle, the debugger keyword should not be left in code files, just in case it causes problems during code execution. However, when investigating a problem, debugger is a useful way to reliably take control of the execution of the application, as demonstrated earlier in this chapter.

In these situations, it doesn’t make sense to disable a rule in the tslint.json file. Instead, a comment that starts with ts:lint-disable-next-line followed by one or more rule names disables rules for the next statement, as shown in Listing 6-13.
import { sum } from "./calc";
let printMessage = (msg: string): void =>  console.log(`Message: ${ msg }`);
let message = ("Hello, TypeScript");
printMessage(message);
// tslint:disable-next-line no-debugger
debugger;
let total = sum(100, 200, 300);
console.log(`Total: ${total}`);
Listing 6-13.

Disabling a Linter Rule for a Single Statement in the index.ts File in the src Folder

The comment in Listing 6-13 tells the linter not to apply the no-debugger rule to the next code statement.

Tip

Rules can also be disabled for all the statements that follow a comment that starts with tslint:disable. You can disable all linting rules by using the tslint:disable or tslint:disable-next-line comment without any rule names.

The Joy And Misery Of Linting

Linters can be a powerful tool for good, especially in a development team with mixed levels of skill and experience. Linters can detect common problems and subtle errors that lead to unexpected behavior or long-term maintenance issues. I like this kind of linting, and I like to run my code through the linting process after I have completed a major application feature or before I commit my code into version control.

But linters can also be a tool of division and strife. In addition to detecting coding errors, linters can be used to enforce rules about indentation, brace placement, the use of semicolons and spaces, and dozens of other style issues. Most developers have style preferences that they adhere to and believe that everyone else should, too. I certainly do: I like four spaces for indentation, and I like opening braces to be on the same line and the expression they relate to. I know that these are part of the “one true way” of writing code, and the fact that other programmers prefer two spaces, for example, has been a source of quiet amazement to me since I first started writing code.

Linters allow people with strong views about formatting to enforce them on others, generally under the banner of being “opinionated.” The logic is that developers spend too much time arguing about different coding styles, and everyone is better off being forced to write in the same way. My experience is that developers will just find something else to argue about and that forcing a code style is often just an excuse to make one person’s preferences mandatory for an entire development team.

I often help readers when they can’t get book examples working (my e-mail address is [email protected] if you need help), and I see all sorts of coding style every week. I know, deep in my heart, that anyone who doesn’t follow my personal coding preferences is just plain wrong. But rather than forcing them to code my way, I get my code editor to reformat the code, which is a feature that every capable editor provides.

My advice is to use linting sparingly and focus on the issues that will cause real problems. Leave formatting decisions to the individuals and rely on code editor reformatting when you need to read code written by a team member who has different preferences.

Unit Testing TypeScript

Some unit test frameworks provide support for TypeScript, although that isn’t as useful as it may sound. Supporting TypeScript for unit testing means allowing tests to be defined in TypeScript files and, sometimes, automatically compiling the TypeScript code before it is tested. Unit tests are performed by executing small parts of an application, and that can be done only with JavaScript since the JavaScript runtime environments have no knowledge of TypeScript features. The result is that unit testing cannot be used to test TypeScript features, which are solely enforced by the TypeScript compiler.

For this book, I have used the Jest test framework, which is easy to use and supports TypeScript tests. Also, with the addition of an extra package, it will ensure that the TypeScript files in the project are compiled into JavaScript before tests are executed. Run the commands shown in Listing 6-14 in the tools folder to install the packages required for testing.
npm install --save-dev [email protected]
npm install --save-dev @types/jest
npm install --save-dev [email protected]
Listing 6-14.

Adding Packages to the Project

The jest package contains the testing framework. The @types/jest package contains the type definitions for the Jest API, which means that tests can be written in TypeScript. The ts-jest package is a plugin to the Jest framework and is responsible for compiling TypeScript files before tests are applied.

Deciding Whether To Unit Test

Unit testing is a contentious topic. This section assumes you do want to do unit testing and shows you how to set up the tools and apply them to TypeScript. It isn’t an introduction to unit testing, and I make no effort to persuade skeptical readers that unit testing is worthwhile. If would like an introduction to unit testing, then there is a good article here: https://en.wikipedia.org/wiki/Unit_testing .

I like unit testing, and I use it in my own projects—but not all of them and not as consistently as you might expect. I tend to focus on writing unit tests for features and functions that I know will be hard to write and that are likely to be the source of bugs in deployment. In these situations, unit testing helps structure my thoughts about how to best implement what I need. I find that just thinking about what I need to test helps produce ideas about potential problems, and that’s before I start dealing with actual bugs and defects.

That said, unit testing is a tool and not a religion, and only you know how much testing you require. If you don’t find unit testing useful or if you have a different methodology that suits you better, then don’t feel you need to unit test just because it is fashionable. (However, if you don’t have a better methodology and you are not testing at all, then you are probably letting users find your bugs, which is rarely ideal.)

Configuring the Test Framework

To configure Jest, add a file named jest.config.js to the tools folder with the content shown in Listing 6-15.
module.exports = {
    "roots": ["src"],
    "transform": {"^.+\.tsx?$": "ts-jest"}
}
Listing 6-15.

The Contents of the jest.config.js File in the tools Folder

The roots setting is used to specify the location of the code files and unit tests. The transform property is used to tell Jest that files with the ts and tsx file extension should be processed with the ts-jest package, which ensures that changes to the code are reflected in tests without needing to explicitly start the compiler. (TSX files are described in Chapter 14.)

Creating Unit Tests

Tests are defined in files that have the test.ts file extension and are conventionally created alongside the code files they relate to. To create a simple unit test for the example application, add a file called calc.test.ts to the src folder and add the code shown in Listing 6-16.
import { sum } from "./calc";
test("check result value", () => {
    let result = sum(10, 20, 30);
    expect(result).toBe(60);
});
Listing 6-16.

The Contents of the calc.test.ts File in the src Folder

Tests are defined using the test function, which is provided by Jest. The test arguments are the name of the test and a function that performs the testing. The unit test in Listing 6-16 is given the name check result value, and the test invokes the sum function with three arguments and inspects the results. Jest provides the expect function that is passed the result and used with a matcher function that specifies the expected result. The matcher in Listing 6-16 is toBe, which tells Jest that the expected result is a specific value. Table 6-2 describes the most useful matcher functions. (The full list of matcher functions can be found at https://jestjs.io/docs/en/expect .)
Table 6-2.

Useful Jest Matcher Functions

Name

Description

toBe(value)

This method asserts that a result is the same as the specified value (but need not be the same object).

toEqual(object)

This method asserts that a result is the same object as the specified value.

toMatch(regexp)

This method asserts that a result matches the specified regular expression.

toBeDefined()

This method asserts that the result has been defined.

toBeUndefined()

This method asserts that the result has not been defined.

toBeNull()

This method asserts that the result is null.

toBeTruthy()

This method asserts that the result is truthy.

toBeFalsy()

This method asserts that the result is falsy.

toContain(substring)

This method asserts that the result contains the specified substring.

toBeLessThan(value)

This method asserts that the result is less than the specified value.

toBeGreaterThan(value)

This method asserts that the result is more than the specified value.

Starting the Test Framework

Unit tests can be run as a one-off task or by using a watch mode that runs the tests when changes are detected. I find the watch mode to be most useful so that I have two command prompts open: one for the output from the compiler and one for the unit tests. To start the tests, open a new command prompt, navigate to the tools folder, and run the command shown in Listing 6-17.
npx jest --watchAll
Listing 6-17.

Starting the Unit Test Framework in Watch Mode

Jest will start, locate the test files in the project, and execute them, producing the following output:
PASS  src/calc.test.ts
  √ check result value (3ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.214s
Ran all test suites.
Watch Usage
 › Press f to run only failed tests.
 › Press o to only run tests related to changed files.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.
The output shows that Jest discovered one test and ran it successfully. When additional tests are defined or when any of the source code in the application changes, Jest will run the tests again and issue a new report. To see what happens when a test fails, make the change shown in Listing 6-18 to the sum function that is the subject of the test.
export function sum(...vals: number[]): number {
    return vals.reduce((total, val) => total += val) + 10;
}
Listing 6-18.

Making a Test Fail in the calc.ts File in the src Folder

The sum function no longer returns the value expected by the unit test, and Jest produces the following warning:
FAIL  src/calc.test.ts
  × check result value (6ms)
  • check result value
    expect(received).toBe(expected) // Object.is equality
    Expected: 60
    Received: 70
      3 | test("check result value", () => {
      4 |     let result = sum(10, 20, 30);
    > 5 |     expect(result).toBe(60);
        |                    ^
      6 | });
      at Object.<anonymous> (src/calc.test.ts:5:20)
Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        4.726s
Ran all test suites.
Watch Usage: Press w to show more.
The output shows the result expected by the test and the result that was actually received. Failed tests can be resolved by fixing the source code to conform to the expectations of the test or, if the purpose of the source code has changed, updating the test to reflect the new behavior. Listing 6-19 modifies the unit test.
import { sum } from "./calc";
test("check result value", () => {
    let result = sum(10, 20, 30);
    expect(result).toBe(70);
});
Listing 6-19.

Changing a Unit Test in the calc.test.ts File in the src Folder

When the change to the test is saved, Jest runs the tests again and reports success.
PASS  src/calc.test.ts
  √ check result value (3ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        5s
Ran all test suites.
Watch Usage: Press w to show more.

Summary

In this chapter, I introduced three tools that are often used to support TypeScript development. The Node.js debugger is a useful way to inspect the state of applications as they are being executed, the linter helps avoid common coding errors that are not detected by the compiler but that cause problems nonetheless, and the unit test framework is used to confirm that code behaves as expected. In the next chapter, I start describing TypeScript features in depth, starting with static type checking.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.218.230