The Invision team has committed to having 100% code coverage for two parts of the system: Reducers and Selectors. These two concepts underlie the primary storage mechanism for the application, a critical system. Any time a reducer or selector is added or modified, the corresponding tests should be amended to reflect these changes. While only selectors and reducers are required to be tested, it is possible to test Action Creators and Visual Components. It is expected that any code that contains sufficiently complex business logic, whether it's in a selector, reducer, action, component, helper or some other block of code, will be tested.
Best Practices
Test Names
Prefer names that describe what the system is doing, rather than naming the tests after specific functions. This allows the tests to double as living documentation of what the system should be doing, as opposed to only what it is doing. Other developers should be able to read test cases to get an understanding of what the feature is designed to do, and what types of behaviors they should expect. While we strive to make the names used in the application code be descriptive of their operation, every developer knows that maintaining this isn't always feasible or warranted. Test descriptions provide a better mechanism to capture this detail.
Do
describe('When the user is making a payment', () => {})
Don't
describe('MakePaymentComponent.makePayment', () => {})
Assertions/Expectations
Because tests assert assumptions about the application code, it's best to isolate assertions (expectations) in a test block to that of a single assumption. This makes it more clear what is expected for a particular test case. Additionally, when a given assertion fails, the testing framework stops checking expectations for a test case. When a given it block has multiple expect statements and the first one fails, it could be masking that the other expect statements are failing as well. The only way to know is fix the broken expect and see if others in that function fail.
Do
it('Should set the user name', () => { ... expect(user.name).to.eql('Bob'); }); it('Should set the available modules on the user', () => { ... expect(user.modules).to.eql([{Id: 1}]); });
Don't
it('Should fetch the user', () => { ... expect(user.name).to.eql('Bob'); expect(user.modules).to.eql([{Id: 1}]); });
Setup
Each test should be an independent unit that passes or fails on its own. Additionally, when a specific test fails, the easier it is to tell why it failed, the easier it will be to diagnose the problem with the system. Therefore, avoid using beforeEach except when absolutely necessary. Instead, prefer to use object creators with sensible defaults that can be called inside of a test, overriding any values as necessary.
One way to organize tests is to use Arrange, Act, Assert.
- Arrange: Set up the necessary preconditions for a test
- Act: Call the function under test
Assert: Verify that after calling the function under test, the correct result occurred
Do
javascriptdescribe('And line of business is loaded', () => { it('Should not fetch the line of business data', () => { const ctrl = createController({ isLineOfBusinessLoaded: true }); ctrl.$onInit(); expect(ctrl.actions.fetchLineOfBusinessMetadata.called).to.be.false; }); });
Don't
javascriptdescribe('And line of business is loaded', () => { let ctrl = {}; beforeEach(() => { ctrl = createController({isLineOfBusinessLoaded: true}); ctrl.$onInit(); }); ... it('Should not fetch the line of business data', () => { expect(ctrl.actions.fetchLineOfBusinessMetadata.called).to.be.false; }); });
The test in the second case attempts to perform all of the setup in the beforeEach, as a result, when the test fails, the only thing the developer knows is that they expected something to be false and it was true. This could be because the test was set up incorrectly, or it could be that the code under test is broken. With the set up code being outside of the it block, it becomes a more difficult task to determine how the test was set up. Additionally, with nested describe blocks, each possibly having a beforeEach, it becomes a more arduous task of tracing through all of the set up block.
Spies, Stubs & Mocks
When using Sinon to mock dependencies, it will replace the functionality on a given object reference. This has impacts outside of the scope of the individual test, as some objects are singletons within the lifespan of the tests. Replacing a function using Sinon on i18n, for instance, will have large impacts on the application. When using spies, stubs or mocks, it is important to restore the functionality after the test is complete. Stubs will allow you to override a function to provide specific functionality for a specific test. If it is not restored after the test is run, it will impact other tests that are using that function.
Do
afterEach(() => { myModule.function.restore && myModule.function.restore(); });
This makes sure that restore exists on this object before trying to restore. This allows the function to be stubbed in some test cases, and not in others, and ensures it is always restored.
Examples
Stubbing i18n.translate
The i18n library is injected via Angular dependencies, and since the tests do not use Angular, the function needs to be stubbed or spied on. If you only have one translation call, you can use a simple stub to have it return a desired string
sinon.stub(i18n, 'translate').returns('My String');
A better approach, would be to use the withArgs function on stubs, to make sure that it matches with the key you'd expect
sinon.stub(i18n, 'translate').withArgs(LocaleKeys.PRODUCT.INTERACTION.INTERACTION).returns('Interaction');
You can then verify that the code under test calls translate with the expected argument (LocaleKeys.PRODUCT.INTERACTION.INTERACTION) and you can then verify that the right string is returned.
Updating State With a Stub
At times, particularly in components, it is common to mock an action call. For the most part these have been verified with a simple expect called to be true type of expectation. However, a better approach would be to use a function inside of the stub that can set the state of your controller. This way your test more accurately reflects what you're after. Instead of verifying that a function was called, you can verify that your controller now has the correct state
sinon.stub(ctrl.actions, 'setCurrentPage', () => { ctrl.state.currentPage = 1; return Promise.resolve(); });
This stubs out the setCurrentPage function. The third parameter passed to the stub in this case is a function that will be called. This allows the test to update the controller's state ({{currentPage}} here, and still resolve a promise. This, in effect, fakes out the function. In sinon 2.0 there is a function callsFake that would do something similar, if and when sinon is upgraded.
Unit Test Debugging
You can run npm run test:debug to use node to debug the mocha tests. It will open a new chrome window and then run the tests. You can then set breakpoints like you would debugging any web page. You'll have to save a file to get the tests to run again.
Please see Dev Machine Setup for details on setting up WebStorm for debugging.