JavaScript test tooling has advanced a lot in the last few years, but tooling can’t solve everything—we still have decisions to make about how to optimally set up our tests. It would be great if we could learn from experienced testers who came before us, but it can be difficult to follow writing about testing in a programming language we aren’t familiar with.
Luckily, there’s one book in particular that has a lot of language-agnostic testing wisdom to share: xUnit Test Patterns. We’ll walk through some of the “test smells” it describes and see examples of how they commonly arise in JavaScript, then we’ll apply the principles from the book to solve these problems. You’ll walk away from this session with more tools in your tool belt to solve testing problems, and clearer language to talk about the tools you already have.
10. 10 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
11. Five Patterns
11 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
12. Takeaways
1. New testing approaches
2. Clearer explanation of the benefits
3. Shared language and support
12 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
13. !
"Code Smells"
13 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
15. 115 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
16. import formatAddress from '../formatAddress';
import addresses from '../__sampleData__/addresses';
describe('formatAddress', () => {
it('formats addresses correctly', () => {
let expectedResult;
for (const address of addresses) {
if (address.street2) {
expectedResult = `${address.street1}
${address.street2}
${address.city}, ${address.province} ${address.postalCode}`;
} else {
expectedResult = `${address.street1}
${address.city}, ${address.province} ${address.postalCode}`;
}
expect(formatAddress(address)).toEqual(expectedResult);
}
});
});
16 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
17. Problems
• Tests can be hard to understand
• Tests might have a bug, and you don't test your tests
• If test data changes, not all cases might be executed
17 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
18. ⚠
Flexible Test
18 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
19. ⚠
Flexible Test
"Using conditional logic to reuse a single test to verify several different
cases."
19 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
20. Solution: Simple Tests
Split the tests into simpler cases, controlling for specific situations.
20 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
21. it('formats an address with two lines correctly', () => {
const address = addresses[0];
const expectedResult = `${address.street1}
${address.street2}
${address.city}, ${address.province} ${address.postalCode}`;
expect(formatAddress(address)).toEqual(expectedResult);
});
21 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
22. it('formats an address with one line correctly', () => {
const address = addresses[1];
const expectedResult = `${address.street1}
${address.city}, ${address.province} ${address.postalCode}`;
expect(formatAddress(address)).toEqual(expectedResult);
});
22 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
24. 224 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
25. import formatAddress from '../formatAddress';
import addresses from '../__sampleData__/addresses';
describe('formatAddress', () => {
it('formats an address with one line correctly', () => {
const address = addresses[1];
const expectedResult = `${address.street1}
${address.city}, ${address.province} ${address.postalCode}`;
expect(formatAddress(address)).toEqual(expectedResult);
});
});
25 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
26. import formatAddress from '../formatAddress';
import addresses from '../__sampleData__/addresses';
describe('formatAddress', () => {
it('formats an address with one line correctly', () => {
const address = addresses[1];
const expectedResult = `${address.street1}
${address.city}, ${address.province} ${address.postalCode}`;
expect(formatAddress(address)).toEqual(expectedResult);
});
});
26 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
27. Problems
• Can't see the data relevant to what's being tested, have to look in
another file
• Test harder to understand, easier for bugs to sneak through
• Coupled to shared test data; if it changes, test could break or give a
false positive
27 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
28. ⚠
Mystery Guest
28 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
29. ⚠
Mystery Guest
“The test reader is not able to see the cause and effect between
fixture and verification logic because part of it is done outside the
Test Method.”
29 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
30. ⚠
Mystery Guest
“The test reader is not able to see the cause and effect between
FIXTURE and verification logic because part of it is done outside the
Test Method.”
30 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
31. Fixture
“everything we need in place to exercise the [production code]”...“the
pre-conditions of the test”
31 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
32. Shared Fixture
“…reusing the same fixture for several or many tests.”
32 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
33. Fresh Fixture
“Each test constructs its own brand-new test fixture for its own
private use.”
33 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
34. ⚠
Mystery Guest
“The test reader is not able to see the cause and effect between fixture
and verification logic because part of it is done outside the Test
Method.”
34 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
35. Solution: set up the data
closer to the test
Several possible approaches using Fresh Fixtures
35 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
36. Approach 1: In-Line Setup
“Each Test Method creates its own Fresh Fixture by [building] exactly
the test fixture it requires.”
36 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
37. it('formats an address with one line correctly', () => {
const address = {
street1: '101 College Street',
city: 'Toronto',
province: 'ON',
postalCode: 'M5G 1L7'
};
const expectedResult = `${address.street1}
${address.city}, ${address.province} ${address.postalCode}`;
expect(formatAddress(address)).toEqual(expectedResult);
});
37 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
38. Approach 2: Delegated Setup
38 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
39. Approach 2: Delegated Setup
39 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
40. Approach 2: Delegated Setup
“Each Test Method creates its own Fresh Fixture by calling Creation
Methods from within the Test Methods.”
40 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
41. function createAddress({ hasStreet2 = false }) {
const address = {
street1: '101 College Street',
city: 'Toronto',
province: 'ON',
postalCode: 'M5G 1L7'
};
if (hasStreet2) {
address.street2 = 'Suite 123';
}
return address;
};
41 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
42. it('formats an address with one line correctly', () => {
const address = createAddress({ hasStreet2: false });
const expectedResult = `${address.street1}
${address.city}, ${address.province} ${address.postalCode}`;
expect(formatAddress(address)).toEqual(expectedResult);
});
42 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
43. it('formats an address with two lines correctly', () => {
const address = createAddress({ hasStreet2: true });
const expectedResult = `${address.street1}
${address.street2}
${address.city}, ${address.province} ${address.postalCode}`;
expect(formatAddress(address)).toEqual(expectedResult);
});
43 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
44. Approach 3: Implicit Setup
“We build the test fixture common to several tests in the setUp
method."
44 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
45. let address;
beforeEach(() => {
address = {
street1: '101 College Street',
city: 'Toronto',
province: 'ON',
postalCode: 'M5G 1L7'
};
});
45 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
46. it('formats an address with one line correctly', () => {
const expectedResult = `${address.street1}
${address.city}, ${address.province} ${address.postalCode}`;
expect(formatAddress(address)).toEqual(expectedResult);
});
46 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
47. it('formats an address with two lines correctly', () => {
address.street2 = 'Apt. 317';
const expectedResult = `${address.street1}
${address.street2}
${address.city}, ${address.province} ${address.postalCode}`;
expect(formatAddress(address)).toEqual(expectedResult);
});
47 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
48. ⚠
Mystery Guest
Solutions:
1. In-Line Setup
2. Delegated Setup
3. Implicit Setup
48 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
49. 349 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
51. Problems
• Hard to see which fields matter and which don't
• Tests get long
51 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
53. ⚠
Irrelevant Information
“The test exposes a lot of irrelevant details about the fixture that
distract the test reader from what really affects the behavior of the
[system under test].”
53 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
54. General Fixture
“The test builds or references a larger fixture than is needed to verify
the functionality in question.”
54 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
55. Solution 1: Minimal Fixture
“We use the smallest and simplest fixture possible for each test.”
55 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
57. it('formats an address correctly', () => {
const address = {
street1: '80 Spanida Avenue',
street2: '4th Floor',
city: 'Toronto',
province: 'ON',
postalCode: 'M5V 2J4'
};
57 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
58. What if that data is still
needed for the code to run?
• Type verification
• Class instance requiring constructor args
• Fields validated by code unrelated to the test
58 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
59. Solution 2: Parameterized
Creation Method
“We set up the test fixture by calling methods that hide the mechanics
of building ready-to-use objects behind Intent-Revealing Names.”
“A Parameterized Creation Method allows the test to pass in some
attributes to be used in the creation of the object. In such a case, we
should pass only those attributes that are expected to affect…the
test's outcome”
59 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
62. Solution 3: Test Data
Creation Libraries
62 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
63. Test Data Bot
!
const { build, sequence } = require('test-data-bot')
const addressBuilder = build('address').fields({
street1: '80 Spanida Avenue',
street2: sequence(x => `Suite ${x}`),
city: 'Toronto',
province: 'ON',
postalCode: 'M5V 2J4',
})
63 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
64. Test Data Bot
!
const user = addressBuilder({ street2: null })
64 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
65. ⚠
Irrelevant Information
Solutions:
1. Minimal Fixture
2. Parameterized Creation Method
3. Test Data Creation Library
65 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
66. 466 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
67. import totalOrder from '../totalOrder';
import orders from '../__sampleData__/orders';
describe('totalOrder', () => {
it('calculates the correct total values when there are no line items', () => {
const order = orders[0];
order.lines = [];
const totalValues = totalOrder(order);
expect(totalValues.subtotal).toEqual(0);
expect(totalValues.tax).toEqual(0);
expect(totalValues.total).toEqual(0);
});
67 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
68. it('calculates the correct total values', () => {
const order = orders[0];
const totalValues = totalOrder(order);
expect(totalValues.subtotal).toEqual(7);
expect(totalValues.tax).toBeCloseTo(1.05);
expect(totalValues.total).toBeCloseTo(8.05);
});
});
68 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
70. ⚠
Interacting Tests
"Tests depend on other tests in some way…[for example,] a test can be
run as part of a suite but cannot be run by itself"
In our case, a test can be run by itself but cannot be run as part of a
suite.
70 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
71. ⚠
Shared Fixture
71 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
73. Solution: Fresh Fixture
“Each test constructs its own brand-new test fixture for its own
private use.”
73 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
74. it('calculates the correct total values when there are no line items', () => {
const order = _.cloneDeep(orders[0]);
order.lines = [];
const totalValues = totalOrder(order);
expect(totalValues.subtotal).toEqual(0);
expect(totalValues.tax).toEqual(0);
expect(totalValues.total).toEqual(0);
});
74 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
75. it('calculates the correct total values', () => {
const order = _.cloneDeep(orders[0]);
const totalValues = totalOrder(order);
expect(totalValues.subtotal).toEqual(7);
expect(totalValues.tax).toBeCloseTo(1.05);
expect(totalValues.total).toBeCloseTo(8.05);
});
});
75 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
81. Problems
• If the logic is wrong in one place, it's wrong in both. Is it really
testing?
• Doesn't let you see the intended result at a glance
81 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
82. ⚠
Production Logic in Test
82 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
83. ⚠
Production Logic in Test
“[If we] use a Calculated Value based on the inputs…we find ourselves
replicating the expected [production] logic inside our test to calculate
the expected values for assertions.”
83 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
88. Warning Sign
⚠
Production Logic in Test
Solution
Hard-Coded Test Data
88 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong
89. Review: Test Patterns
1. Flexible Test → Simple Tests
2. Mystery Guest → In-Line Setup
3. Irrelevant Information → Minimal Fixture
4. Interacting Tests → Fresh Fixture
5. Production Logic in Test → Hard-Coded Test Data
89 Old Solutions to New Testing Problems - Assert(js) 2019 - @CodingItWrong