-
Notifications
You must be signed in to change notification settings - Fork 0
Unit test examples #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
# Unit Testing Examples | ||
|
||
## Code we will be testing | ||
|
||
In this example, we will set up an API endpoint which checks if the password is | ||
valid according to business logic rules. The rules are that password needs to be | ||
between 5 and 255 characters with at least one uppercase letter, at least one | ||
lowercase letter, at least 2 numbers and at least 2 special characters. | ||
|
||
[Sandbox Code Examples 🔗](https://stackblitz.com/edit/node-jwphcjdk?file=unit-tests.js) | ||
|
||
```javascript | ||
// password-validator.js | ||
|
||
import PasswordValidator from 'password-validator'; | ||
|
||
const MIN_LENGTH = 10; | ||
const MAX_LENGTH = 255; | ||
|
||
export function validatePassword(password) { | ||
const passwordValidator = new PasswordValidator() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no benefit to instantiating this schema each time the method is called, it can just be done once above the function |
||
.is().min(MIN_LENGTH) | ||
.is().max(MAX_LENGTH) | ||
.has().uppercase() | ||
.has().lowercase() | ||
.has().digits(2) | ||
.has().symbols(2); | ||
const result = passwordValidator.validate(password, { details: true }); | ||
return { isValid: result.length === 0, details: result }; | ||
} | ||
``` | ||
|
||
```javascript | ||
// app.js | ||
|
||
import http from 'http'; | ||
|
||
const PORT = 8000; | ||
|
||
async function handleRequest(req, res) { | ||
try { | ||
const { isValid, details } = validatePassword(req.body?.password); | ||
const statusCode = isValid ? 200 : 400; | ||
res.writeHead(statusCode, { 'Content-Type': 'application/json' }); | ||
if (statusCode === 400) res.write(JSON.stringify(details)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} catch { | ||
res.writeHead(500); | ||
} | ||
res.end(); | ||
} | ||
|
||
http | ||
.createServer(handleRequest) | ||
.listen(PORT, () => console.log('App running on port', PORT)); | ||
``` | ||
|
||
## Writing tests | ||
|
||
Let's write some tests for the `validatePassword` function to make sure it works | ||
as described. | ||
|
||
```javascript | ||
import { describe, mock, it } from 'node:test'; | ||
import assert from 'node:assert/strict'; | ||
import { validatePassword } from './password-validator.js'; | ||
|
||
describe('validatePassword', function() { | ||
describe('successfully validates password when it', function() { | ||
it('is "Testing12!?"', function() { | ||
const { isValid } = validatePassword('Testing12!?'); | ||
assert.equal(isValid, true); | ||
}); | ||
}); | ||
|
||
describe('fails to validate password when it', function() { | ||
it('is not the correct length', function() { | ||
const { isValid } = validatePassword('Test12!?'); | ||
assert.equal(isValid, false); | ||
}); | ||
|
||
it('does not contain uppercase letter', function() { | ||
const { isValid } = validatePassword('test12!?'); | ||
assert.equal(isValid, false); | ||
}); | ||
}); | ||
}); | ||
Comment on lines
+67
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While running the tests, this would look like:
When writing tests, it's often not as good to DRY the code as it is with source code. The worst perpetrator usually are the generic setup functions that are extracted just because a few lines are often used together, but it also applies in the general separation of test cases (drying with
Here, every test stands as a single readable test, without having to remember the Another reason to not use this is that
The above is an example of integration tests, and using Another change I did was the removal of explicit usage of test data within the test name - it isn't important for the reader to know the exact string tested, what's important is knowing that the password is valid. The rules for password being valid are many, so even putting a specific string doesn't give much info and might be misleading since it could be considered a special case. I've also reworded Lastly, I've started every test with Not sure if what I wrote here is also something to mention in the antipattern section, since the code does not seem wrong per se, it just seems like it can provide more information if structured differently, at least based on my experience. @bubafinder I know you already approved, but would like to hear your opinion as well. |
||
``` | ||
|
||
## Antipatterns | ||
|
||
### Antipattern 1 | ||
|
||
Unit testing handle request function. We don't want to unit test the `handleRequest` | ||
function which handles routing and HTTP request/response. We can test that with API | ||
and/or integration tests. | ||
|
||
### Antipattern 2 | ||
|
||
We write single unit test for each scenario. While this example is overly simple | ||
and maybe we don't need 20 unit tests to cover this function, it is a good idea | ||
to split testing for each criteria. For example, one test to cover password min | ||
length, one to cover uppercase letter, one to cover min number of required digits | ||
etc. When tests are written like this, it is easier to pinpoint which criteria | ||
caused the code to fail and how to fix it. | ||
|
||
```javascript | ||
// WARNING: this is an example of an antipattern, do not write tests like this | ||
|
||
describe('validatePassword', function() { | ||
describe('successfully validates password when it', function() { | ||
it('is valid', function() { | ||
const { isValid } = validatePassword('Testing12!?'); | ||
assert.equal(isValid, true); | ||
}); | ||
}); | ||
|
||
describe('fails to validate password when it', function() { | ||
it('is invalid', function() { | ||
const { isValid } = validatePassword(' '); | ||
assert.equal(isValid, false); | ||
}); | ||
}); | ||
}); | ||
``` | ||
|
||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.