Skip to content

Commit 55e87d8

Browse files
authored
Merge pull request #8 from ReCoded-Org/testingmodule
Module 5: Testing
2 parents 85a6b24 + 2fa133d commit 55e87d8

File tree

9 files changed

+439
-0
lines changed

9 files changed

+439
-0
lines changed
20 KB
Loading
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# What is a test?
2+
In programming, a test refers to a piece of code that can be run to verify the
3+
behavior of a program. For example, if you write a function, and you want to be
4+
sure that it works for various inputs and outputs, you can write a test for this
5+
function.
6+
7+
Tests themselves are code. At larger companies, every piece of submitted code
8+
needs to submitted with a corresponding test. A general rule of thumb is that,
9+
if you change a line of your code so that the behavior is wrong, a test
10+
somewhere should fail, so that such a mistake can be caught.
11+
12+
Tests are an important part of many companies' infrastructure for development.
13+
Generally, humans are not manually running these tests, though you may manually
14+
run it before sending a PR or while working on the test. In a process called
15+
**continuous integration** (CI), these tests will automatically run after every
16+
pull request is proposed. This means that generally, if your code and pull
17+
request are not passing the tests, it cannot be merged in.
18+
19+
There are many types of tests: unit tests, integration tests, screenshot tests,
20+
and so on. In a later section, we will talk about some of these types of tests.
21+
Most commonly, we will refer to unit tests, which are the most frequent type of
22+
tests.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Why is testing important?
2+
3+
You may think it is sufficient simply to manually test your code.
4+
5+
There are a few reasons testing is very important when it comes to a growing
6+
codebase. You should not imagine the situation where you are working on a
7+
project with one or two people, but a situation where you are working with a
8+
larger team and with code that the company will use the code long after you leave. You
9+
may not be at the company five years later, or you may forget what the code does
10+
in six months.
11+
12+
## Correctness
13+
Most obviously, a test is used to verify the correctness of the code. If you
14+
have ever changed your code then had to test three or more cases manually, then
15+
you are familiar with the fact that it is quite cumbersome to repeatedly check
16+
manually. Not only that, it's very easy to forget to check some case when a
17+
human is doing it by hand.
18+
19+
In contract, when you write a test, you simply need to run a command in order to
20+
initiate the tests again, rather than repeating some steps to ensure that your
21+
code is working.
22+
23+
If you work at a company with code review, consider the perspective of the
24+
person reviewing your code: how do they know your code doesn't have bugs? How do
25+
they know it works correctly? If your code is submitted along with tests, the
26+
reviewer can be aware of exactly what you have or haven't verified about your
27+
code.
28+
29+
## Ease of change
30+
Have you ever
31+
felt scared to change code, because you weren't sure if it would still work for
32+
all the cases after you changed it? With a good test suite, this fear generally
33+
doesn't exist. Even if the code is rewritten but the same tests pass, indicating
34+
that the new code has the same behavior, one can feel a lot more secure about
35+
rewriting the code.
36+
37+
Having good tests allows code to be changed in a more robust fashion. When the
38+
same tests pass, a developer can be more sure (perhaps not completely sure,
39+
depending on the tests) that changing the code did not break anything.
40+
Additionally, it allows people who are not familiar with the code (say, your
41+
teammates who work on related code, or someone who is responsible for your code
42+
five years later) to more easily work with the code.
43+
44+
## Documentation
45+
A well-written test suite serves as documentation. Again, imagine you have just
46+
joined a company, and you don't know what a function or component does. In fact, if you want to
47+
understand what a piece of code does, it's often more productive to go read the
48+
tests first, rather than the code itself.
49+
50+
In the next section, we'll look an
51+
example of a test, which will help illustrate how the tests themselves can be
52+
used to document the code.
53+
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Example code
2+
3+
In this section, we'll take a look at some example code for tests. It's possible
4+
you've never seen a unit test before, but you'll find that they're actually
5+
quite readable, even without knowing the framework.
6+
7+
Let's take a look at this example from
8+
[Jest](https://jestjs.io/docs/getting-started), one of the most popular testing
9+
frameworks in JavaScript. Most testing frameworks work in approximately the same
10+
fashion, simply using different syntax.
11+
12+
```javascript
13+
test('adds 1 + 2 to equal 3', () => {
14+
expect(sum(1, 2)).toBe(3);
15+
});
16+
```
17+
18+
What does this test do? To start off, consider what a function called `sum`
19+
should do. Even though we can't see the implementation, this is an example of
20+
how simply reading the test can tell you how the function should behave.
21+
22+
In this test, when the sum function is given the arguments `sum(1, 2)`, we call
23+
a function `expect` and assert the result `toBe` `3`. Even above that, there is
24+
a written string (anything can be written there), that tells us that `adds 1 + 2
25+
to equal 3`. Again, even though we have no idea how sum is written internally
26+
(although in this case, writing such a function should be quite trivial), **only
27+
from reading the test**, the behavior of the function is documented in some
28+
fashion.
29+
30+
Let's continue reading some tests, without even looking at the
31+
implementation of the tested functions for illustrative purposes.
32+
33+
In this example, let's look at the function `absolute`. You may be able to guess
34+
what the function does from the name, but if not, read the code below, and try
35+
to figure out what it does.
36+
37+
```javascript
38+
describe('absolute', () => {
39+
it('should return positive number if input positive', () => {
40+
const result = absolute(1);
41+
expect(result).toBe(1);
42+
});
43+
it('should return positive number if input negative', () => {
44+
const result = absolute(-1);
45+
expect(result).toBe(1);
46+
});
47+
it('should return zero if input is zero', () => {
48+
const result = absolute(0);
49+
expect(result).toBe(0);
50+
});
51+
});
52+
```
53+
54+
In this example, the `describe` function is used to group together a related set
55+
of tests. Note how the code almost reads like a written description of the
56+
behavior of the test.
57+
58+
Finally, let's read one more example of a slightly more complex test, written in
59+
TypeScript. Again, understanding every line is not important, and some
60+
implementations, such as `makeExpressRequest`, have been omitted. But the
61+
important part is to try to read the test and see how it helps us understand the
62+
behavior of a function.
63+
64+
```typescript
65+
describe('makeExpressRequest()', () => {
66+
it('should return token when cookie token is provided', () => {
67+
const request = makeExpressRequest('111.222.333', null);
68+
expect(getToken(request)).toStrictEqual('111.222.333');
69+
});
70+
71+
it('should extract token when bearer schema is provided', () => {
72+
const request = makeExpressRequest(null, 'Bearer xxx.yyy.zzz');
73+
expect(getToken(request)).toStrictEqual('xxx.yyy.zzz');
74+
});
75+
76+
it('should throw error when the both are provided', () => {
77+
const request = makeExpressRequest('111.222.333', 'Bearer xxx.yyy.zzz');
78+
const expectToThrow = async () => getToken(request);
79+
expect.assertions(1);
80+
return expectToThrow().catch((e) => {
81+
expect(e).toBeDefined();
82+
});
83+
});
84+
85+
it('should return null if none is provided', () => {
86+
const request = makeExpressRequest(null, null);
87+
expect(getToken(request)).toBeNull();
88+
});
89+
});
90+
```
91+
92+
Try to answer the following questions about the above example:
93+
* What does the first argument to `makeExpressRequest` represent?
94+
* What does the second argument represent?
95+
* Are you supposed to pass in both arguments at once?
96+
* If you pass in the second argument, what does it do to the argument?
97+
98+
By reading the tests in this section, this has given you a more concrete
99+
understanding of what tests may look like, how they verify correctness, and how
100+
they help others understand the behavior of a function.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Testing Philosophy
2+
In this section, we'll discuss some aspects of testing philosophy. There are
3+
many different ways to approach testing.
4+
5+
In real life, you may perhaps work at
6+
companies that do almost no testing (this is not good, but it is the reality).
7+
This is especially true in smaller companies. You may work at a company that
8+
chooses only to test certain components or write certain types of tests.
9+
Finally, as mentioned previously, at some companies, especially bigger ones, you
10+
may be required to write tests so that every line of code is covered by a test.
11+
12+
Simply writing a test is not sufficient: there are also good and bad ways to
13+
write tests. In the sections ahead, we'll consider some additional factors about
14+
the testing process and the quality of tests.
15+
16+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Test-driven Development
2+
3+
**Test-driven development** (TDD) is a process where developers write the tests first,
4+
**before** writing the code. As we saw in the last section, the tests alone can
5+
be sufficient for specifying the behavior of the function.
6+
7+
Even to modify existing code, a test would be written first under this
8+
philosophy. For example, if you had an `if` statement in your code already, but
9+
you wanted to add an `else` branch, under TDD, you would first write a test for
10+
that `else` branch, and then write the actual code. As mentioned before, at the
11+
highest levels of testing, a mistake in any line of code should be caught, and
12+
TDD facilitates this.
13+
14+
## Why TDD?
15+
16+
First, TDD requires the developer to first think
17+
about the behavior of the tested code and how it will be used, rather than
18+
rushing head-first into writing the code. Perhaps you may have experienced
19+
writing code and realizing later that the code you wrote was not quite what you
20+
needed. When you think carefully first about the behavior of the code as in TDD,
21+
this situation is often avoided.
22+
23+
Second, as developers must write tests with every code, TDD requires developers
24+
to write tests and puts developers in the habit of writing tests, which is
25+
generally healthy for the robustness of a codebase. If TDD was not present,
26+
depending on the company, developers may be less strict about writing tests.
27+
28+
## Is TDD used in the real world?
29+
Many companies do adopt TDD, although in practice, this is a minority of
30+
companies. TDD is far from a perfect philosophy: it does not guarantee good test
31+
coverage and it may even encourage developers to write sloppier tests so that
32+
they can simply start coding faster.
33+
34+
However, as with all things
35+
in life, understanding the perspective of TDD is still extremely useful.
36+
But as all things in life, learning different perspectives is useful.
37+
TDD is on the very rigorous end of the role of testing in
38+
development. In reality, you will probably end up somewhere in between --
39+
perhaps sometimes you will think about the tests first, or you will think in
40+
your head about what will be tested, even though you may not be required to
41+
actually write the tests as you would in TDD.
42+
43+
The reason that understanding or trying test-driven development is helpful is
44+
because even if you do not work at a company that adopts test-driven
45+
development, it will encourage you to think about success and failure cases
46+
while you write your code. **This is a crucial part of the programmer's
47+
mindset.** As you become a more experienced programmer, you will try to predict
48+
issues; test-driven development aids greatly with this mindset, since it
49+
encourages you to write the test and think about these scenarios before writing
50+
the code.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Properties of a good test
2+
As mentioned previously, simply writing a test is not the same as writing a good
3+
test. In this section, we'll examine some properties of what makes a good test.
4+
5+
## Size: Focused tests
6+
A well-written test should not test too many things at once. In general, test
7+
cases should be well-isolated and have a [single
8+
responsibility](https://en.wikipedia.org/wiki/Single-responsibility_principle)
9+
or intention.
10+
11+
Consider the previous example of the `absolute()` function. Suppose the test
12+
instead looked like this:
13+
14+
```javascript
15+
describe('absolute', () => {
16+
it('should correctly handle inputs', () => {
17+
const result1 = absolute(1);
18+
expect(result0).toBe(1);
19+
const result1 = absolute(-1);
20+
expect(result1).toBe(1);
21+
const result2 = absolute(0);
22+
expect(result2).toBe(0);
23+
});
24+
});
25+
```
26+
27+
Now compare this to the original. Which one do you find more readable and
28+
organized? While this is a simple example, separating the cases for clarity
29+
becomes especially important with more complex functions.
30+
31+
```javascript
32+
describe('absolute', () => {
33+
it('should return positive number if input positive', () => {
34+
const result = absolute(1);
35+
expect(result).toBe(1);
36+
});
37+
it('should return positive number if input negative', () => {
38+
const result = absolute(-1);
39+
expect(result).toBe(1);
40+
});
41+
it('should return zero if input is zero', () => {
42+
const result = absolute(0);
43+
expect(result).toBe(0);
44+
});
45+
});
46+
```
47+
48+
## Redundancy: avoid overlapping test cases
49+
As a related topic, you usually want to avoid test cases that overlap too much
50+
with existing test cases. For example, consider the following test case for the
51+
above. Is it useful, or is it redundant, overlapping with one of the test cases
52+
above?
53+
54+
```javascript
55+
it('should return positive number if input is large and negative', () => {
56+
const result = absolute(-100);
57+
expect(result).toBe(100);
58+
});
59+
```
60+
61+
This case is redundant. Testing `absolute(-1)` and `absolute(-100)` is not very
62+
useful; the two test cases overlap. We want to avoid this.
63+
64+
It is often asked how much overlap is okay. Inevitably, some test cases will
65+
overlap with each other. There is no correct answer here: just know that some
66+
overlap is okay, but you want to do your best to avoid redundancy by selecting
67+
test cases that are representative of the cases of your function.
68+
69+
In the
70+
programmer's mindset, usually there are a set of possible scenarios to consider.
71+
For example, in the case of the `absolute()` function, there are three distinct
72+
scenarios that define the behavior: positive, negative, and zero. Whether the
73+
negative number is large or not makes no difference.
74+
75+
## Thoroughness
76+
In this module, it was mentioned several times that good test coverage means
77+
that, if the code is behaving incorrectly, even in one line or in one
78+
if-condition, some line will fail.
79+
80+
A common problem with tests is that some case is missing. Perhaps when certain
81+
arguments are passed to your function, it behaves differently. A good suite of
82+
tests will make sure to cover these cases.
83+
84+
## Organization: Arrange-Act-Assert
85+
Tests generally follow approximately the same pattern, and this has been
86+
crystallized in a commonly adopted idea called ["Arrange Act
87+
Assert."](https://automationpanda.com/2020/07/07/arrange-act-assert-a-pattern-for-writing-good-tests/)
88+
89+
1. **Arrange**: Many tests require some type of setup; the setup should come
90+
initially.
91+
2. **Act**: In the act step, the actual behavior being tested should be invoked,
92+
such as a function call, an API call, a component render, etc.
93+
3. **Assert**: Finally, to ensure correctness, assert the expected outcomes.
94+
This was seen earlier using the `expect` function. Expect and assert, in this
95+
context, are synonymous.
96+
97+
## Additional readings
98+
This [StackOverflow
99+
question](https://stackoverflow.com/questions/61400/what-makes-a-good-unit-test)
100+
gives some interesting insights into what are considered good properties of a
101+
unit test.
102+
103+
Google has a good resource called *Testing on the Toilet*. On the back of the
104+
door in every toilet stall at Google, you can find a one-page flyer that
105+
describes some aspect of testing (that's how important testing is!). These are
106+
publicized. Some good ones for junior developers are included below:
107+
108+
- [Keep tests focused](https://testing.googleblog.com/2018/06/testing-on-toilet-keep-tests-focused.html)
109+
- [DAMP](https://www.googblogs.com/testing-on-the-toilet-tests-too-dry-make-them-damp/). Don't overuse functions in test code, focus on readability.
110+
- [Just say no to end-to-end tests](https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html). Avoid overusing tests that connect too many parts of your system.
111+

0 commit comments

Comments
 (0)