# js-unit-testing-guide **Repository Path**: git-hub-image/js-unit-testing-guide ## Basic Information - **Project Name**: js-unit-testing-guide - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-03-18 - **Last Updated**: 2021-09-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 📙 A guide to unit testing in JavaScript Translations: 🇨🇳 [Chinese (Simplified)](https://github.com/mawrkus/js-unit-testing-guide/tree/master/translations/zh-cn/README.md) (thanks to [GabrielchenCN](https://github.com/GabrielchenCN)) This is a living document, **new ideas are always welcome**. Contribute: fork, clone, branch, commit, push, pull request! *All the information provided has been compiled & adapted from the references cited at the end of the document. The guidelines are illustrated by my own examples, fruit of my personal experience writing and reviewing unit tests. Many thanks to all of the sources of information & contributors!* ## 📖 Table of contents 1. General principles + [Unit tests](#unit-tests) + [Design principles](#design-principles) 2. Guidelines + [Whenever possible, use TDD](#whenever-possible-use-tdd) + [Structure your tests properly](#structure-your-tests-properly) + [Name your tests properly](#name-your-tests-properly) + [Don't comment out tests](#dont-comment-out-tests) + [Avoid logic in your tests](#avoid-logic-in-your-tests) + [Don't write unnecessary expectations](#dont-write-unnecessary-expectations) + [Properly setup the actions that apply to all the tests involved](#properly-setup-the-actions-that-apply-to-all-the-tests-involved) + [Consider using factory functions in the tests](#consider-using-factory-functions-in-the-tests) + [Know your testing framework API](#know-your-testing-framework-api) + [Don't test multiple concerns in the same test](#dont-test-multiple-concerns-in-the-same-test) + [Cover the general case and the edge cases](#cover-the-general-case-and-the-edge-cases) + [When applying TDD, always start by writing the simplest failing test](#when-applying-tdd-always-start-by-writing-the-simplest-failing-test) + [When applying TDD, always make small steps in each test-first cycle](#when-applying-tdd-always-make-small-steps-in-each-test-first-cycle) + [Test the behaviour, not the internal implementation](#test-the-behaviour-not-the-internal-implementation) + [Don't mock everything](#dont-mock-everything) + [Create new tests for every defect](#create-new-tests-for-every-defect) + [Don't write unit tests for complex user interactions](#dont-write-unit-tests-for-complex-user-interactions) + [Test simple user actions](#test-simple-user-actions) + [Review test code first](#review-test-code-first) + [Practice code katas, learn with pair programming](#practice-code-katas-learn-with-pair-programming) 3. [Resources](#-resources) ## General principles ### Unit tests **Unit = Unit of work** This could involve **multiple methods and classes** invoked by some public API that can: + Return a value or throw an exception + Change the state of the system + Make 3rd party calls (API, database, ...) A unit test should test the behaviour of a unit of work: for a given input, it expects an end result that can be any of the above. **Unit tests are isolated and independent of each other** + Any given behaviour should be specified in **one and only one test** + The execution/order of execution of one test **cannot affect the others** The code is designed to support this independence (see "Design principles" below). **Unit tests are lightweight tests** + Repeatable + Fast + Consistent + Easy to write and read **Unit tests are code too** They should meet the same level of quality as the code being tested. They can be refactored as well to make them more maintainable and/or readable. • [Back to ToC](#-table-of-contents) • ### Design principles The key to good unit testing is to write **testable code**. Applying simple design principles can help, in particular: + Use a **good naming** convention and **comment** your code (the "why?" not the "how"), keep in mind that comments are not a substitute for bad naming or bad design + **DRY**: Don't Repeat Yourself, avoid code duplication + **Single responsibility**: each object/function must focus on a single task + Keep a **single level of abstraction** in the same component (for example, do not mix business logic with lower-level technical details in the same method) + **Minimize dependencies** between components: encapsulate, interchange less information between components + **Support configurability** rather than hard-coding, this prevents having to replicate the exact same environment when testing (e.g.: markup) + Apply adequate **design patterns**, especially **dependency injection** that allows separating an object's creation responsibility from business logic + Avoid global mutable state • [Back to ToC](#-table-of-contents) • ## Guidelines The goal of these guidelines is to make your tests: + **Readable** + **Maintainable** + **Trustworthy** These are the 3 pillars of good unit testing. All the following examples assume the usage of the [Jasmine](http://jasmine.github.io) framework. • [Back to ToC](#-table-of-contents) • --------------------------------------- ### Whenever possible, use TDD TDD is a _design process_, not a testing process. TDD is a robust way of designing software components ("units") interactively so that their behaviour is specified through unit tests. How? Why? #### Test-first cycle 1. Write a simple failing test 2. Make the test pass by writing the minimum amount of code, don't bother with code quality 3. Refactor the code by applying design principles/patterns #### Consequences of the test-first cycle + Writing a test first makes the code design testable de facto + Writing just the amount of code needed to implement the required functionality makes the resulting codebase minimal, thus more maintainable + The codebase can be enhanced using refactoring mechanisms, the tests give you confidence that the new code is not modifying the existing functionalities + Cleaning the code in each cycle makes the codebase more maintainable, it is much cheaper to change the code frequently and in small increments + Fast feedback for the developers, you know that you don't break anything and that you are evolving the system in a good direction + Generates confidence to add features, fix bugs, or explore new designs Note that code written without a test-first approach is often very hard to test. • [Back to ToC](#-table-of-contents) • ### Structure your tests properly Don't hesitate to nest your suites to structure logically your tests in subsets. **:(** ```js describe('A set of functionalities', () => { it('a set of functionalities should do something nice', () => { }); it('a subset of functionalities should do something great', () => { }); it('a subset of functionalities should do something awesome', () => { }); it('another subset of functionalities should also do something great', () => { }); }); ``` **:)** ```js describe('A set of functionalities', () => { it('should do something nice', () => { }); describe('A subset of functionalities', () => { it('should do something great', () => { }); it('should do something awesome', () => { }); }); describe('Another subset of functionalities', () => { it('should also do something great', () => { }); }); }); ``` • [Back to ToC](#-table-of-contents) • ### Name your tests properly Tests names should be concise, explicit, descriptive and in correct English. Read the output of the spec runner and verify that it is understandable! Keep in mind that someone else will read it too. Tests can be the live documentation of the code. **:(** ```js describe('MyGallery', () => { it('init set correct property when called (thumb size, thumbs count)', () => { }); // ... }); ``` **:)** ```js describe('The Gallery instance', () => { it('should properly calculate the thumb size when initialized', () => { }); it('should properly calculate the thumbs count when initialized', () => { }); // ... }); ``` In order to help you write test names properly, you can use the **"unit of work - scenario/context - expected behaviour"** pattern: ```js describe('[unit of work]', () => { it('should [expected behaviour] when [scenario/context]', () => { }); }); ``` Or whenever you have many tests that follow the same scenario or are related to the same context: ```js describe('[unit of work]', () => { describe('when [scenario/context]', () => { it('should [expected behaviour]', () => { }); }); }); ``` For example: **:) :)** ```js describe('The Gallery instance', () => { describe('when initialized', () => { it('should properly calculate the thumb size', () => { }); it('should properly calculate the thumbs count', () => { }); }); // ... }); ``` • [Back to ToC](#-table-of-contents) • ### Don't comment out tests Never. Ever. Tests have a reason to be or not. Don't comment them out because they are too slow, too complex or produce false negatives. Instead, make them fast, simple and trustworthy. If not, remove them completely. • [Back to ToC](#-table-of-contents) • ### Avoid logic in your tests Always use simple statements. Don't use loops and/or conditionals. If you do, you add a possible entry point for bugs in the test itself: + Conditionals: you don't know which path the test will take + Loops: you could be sharing state between tests **:(** ```js it('should properly sanitize strings', () => { let result; const testValues = { 'Avion' : 'Avi' + String.fromCharCode(243) + 'n', 'The-space' : 'The space', 'Weird-chars-' : 'Weird chars!!', 'file-name.zip' : 'file name.zip', 'my-name.zip' : 'my.name.zip' }; for (result in testValues) { expect(sanitizeString(testValues[result])).toBe(result); } }); ``` **:)** ```js it('should properly sanitize strings', () => { expect(sanitizeString('Avi'+String.fromCharCode(243)+'n')).toBe('Avion'); expect(sanitizeString('The space')).toBe('The-space'); expect(sanitizeString('Weird chars!!')).toBe('Weird-chars-'); expect(sanitizeString('file name.zip')).toBe('file-name.zip'); expect(sanitizeString('my.name.zip')).toBe('my-name.zip'); }); ``` Better: write a test for each type of sanitization. It will give a nice output of all possible cases, improving maintainability. **:) :)** ```js it('should sanitize a string containing non-ASCII chars', () => { expect(sanitizeString('Avi'+String.fromCharCode(243)+'n')).toBe('Avion'); }); it('should sanitize a string containing spaces', () => { expect(sanitizeString('The space')).toBe('The-space'); }); it('should sanitize a string containing exclamation signs', () => { expect(sanitizeString('Weird chars!!')).toBe('Weird-chars-'); }); it('should sanitize a filename containing spaces', () => { expect(sanitizeString('file name.zip')).toBe('file-name.zip'); }); it('should sanitize a filename containing more than one dot', () => { expect(sanitizeString('my.name.zip')).toBe('my-name.zip'); }); ``` • [Back to ToC](#-table-of-contents) • ### Don't write unnecessary expectations Remember, unit tests are a design specification of how a certain *behaviour* should work, not a list of observations of everything the code happens to do. **:(** ```js it('should multiply the number passed as parameter and subtract one', () => { const multiplySpy = spyOn(Calculator, 'multiple').and.callThrough(); const subtractSpy = spyOn(Calculator, 'subtract').and.callThrough(); const result = Calculator.compute(21.5); expect(multiplySpy).toHaveBeenCalledWith(21.5, 2); expect(subtractSpy).toHaveBeenCalledWith(43, 1); expect(result).toBe(42); }); ``` **:)** ```js it('should multiply the number passed as parameter and subtract one', () => { const result = Calculator.compute(21.5); expect(result).toBe(42); }); ``` This will improve maintainability. Your test is no longer tied to implementation details. • [Back to ToC](#-table-of-contents) • ### Properly setup the actions that apply to all the tests involved **:(** ```js describe('Saving the user profile', () => { let profileModule; let notifyUserSpy; let onCompleteSpy; beforeEach(() => { profileModule = new ProfileModule(); notifyUserSpy = spyOn(profileModule, 'notifyUser'); onCompleteSpy = jasmine.createSpy(); }); it('should send the updated profile data to the server', () => { jasmine.Ajax.install(); profileModule.save(); const request = jasmine.Ajax.requests.mostRecent(); expect(request.url).toBe('/profiles/1'); expect(request.method).toBe('POST'); expect(request.data()).toEqual({ username: 'mawrkus' }); jasmine.Ajax.uninstall(); }); it('should notify the user', () => { jasmine.Ajax.install(); profileModule.save(); expect(notifyUserSpy).toHaveBeenCalled(); jasmine.Ajax.uninstall(); }); it('should properly execute the callback passed as parameter', () => { jasmine.Ajax.install(); profileModule.save(onCompleteSpy); jasmine.Ajax.uninstall(); expect(onCompleteSpy).toHaveBeenCalled(); }); }); ``` The setup code should apply to all the tests: **:)** ```js describe('Saving the user profile', () => { let profileModule; beforeEach(() => { jasmine.Ajax.install(); profileModule = new ProfileModule(); }); afterEach( () => { jasmine.Ajax.uninstall(); }); it('should send the updated profile data to the server', () => { profileModule.save(); const request = jasmine.Ajax.requests.mostRecent(); expect(request.url).toBe('/profiles/1'); expect(request.method).toBe('POST'); }); it('should notify the user', () => { spyOn(profileModule, 'notifyUser'); profileModule.save(); expect(profileModule.notifyUser).toHaveBeenCalled(); }); it('should properly execute the callback passed as parameter', () => { const onCompleteSpy = jasmine.createSpy(); profileModule.save(onCompleteSpy); expect(onCompleteSpy).toHaveBeenCalled(); }); }); ``` Consider keeping the setup code minimal to preserve readability and maintainability. • [Back to ToC](#-table-of-contents) • ### Consider using factory functions in the tests Factories can: + help reduce the setup code, especially if you use dependency injection + make each test more readable, since the creation is a single function call that can be in the test itself instead of the setup + provide flexibility when creating new instances (setting an initial state, for example) There's a trade-off to find here between applying the DRY principle and readability. **:(** ```js describe('User profile module', () => { let profileModule; let pubSub; beforeEach(() => { const element = document.getElementById('my-profile'); pubSub = new PubSub({ sync: true }); profileModule = new ProfileModule({ element, pubSub, likes: 0 }); }); it('should publish a topic when a new "like" is given', () => { spyOn(pubSub, 'notify'); profileModule.incLikes(); expect(pubSub.notify).toHaveBeenCalledWith('likes:inc', { count: 1 }); }); it('should retrieve the correct number of likes', () => { profileModule.incLikes(); profileModule.incLikes(); expect(profileModule.getLikes()).toBe(2); }); }); ``` **:)** ```js describe('User profile module', () => { function createProfileModule({ element = document.getElementById('my-profile'), likes = 0, pubSub = new PubSub({ sync: true }) }) { return new ProfileModule({ element, likes, pubSub }); } it('should publish a topic when a new "like" is given', () => { const pubSub = jasmine.createSpyObj('pubSub', ['notify']); const profileModule = createProfileModule({ pubSub }); profileModule.incLikes(); expect(pubSub.notify).toHaveBeenCalledWith('likes:inc'); }); it('should retrieve the correct number of likes', () => { const profileModule = createProfileModule({ likes: 40 }); profileModule.incLikes(); profileModule.incLikes(); expect(profileModule.getLikes()).toBe(42); }); }); ``` Factories are particularly useful when dealing with the DOM: **:(** ```js describe('The search component', () => { describe('when the search button is clicked', () => { let container; let form; let searchInput; let submitInput; beforeEach(() => { fixtures.inject(`