People (a script) ==> Macros (automated scripts) ==> Unit Testing (starting 1990's)
Rushing, emergency pushing, spray and pray, and other ill-advised approaches to coding all can become a horror story!
Avoid a horror story with...
Benefits:
- Ensure code is working
- Documents what code is supposed to do
- Tests offer precision in behavior
- Don't break what's already working (avoiding regressions)
- Forces you to think about how your code will function in advance
- For your own peace of mind!
A testing library suite really just has:
- Labels (
describe
) - Functions (
it
) - Assertions (
expect
)
Assertions like expect
is really just a thing that throws errors! It's remarkably simple.
We assert
a state or value. If an error is thrown, we throw it and show it to the user. If not, then we say 'It was a pass'.
Writing tests before you write your code. It grounds you in your goals: because you're writing your tests first. You always end up with automated tests because you're writing them from the start!
Some studies suggest that TDD helps modularity. You often have to refactor as you move through tests. This creates a process of continuous refinement, where you end up with simpler, more modular code.
TDD also has disadvantages:
- It's HARD!
- Requirements may change (a lot), quickly making our tests obsolete.
- Harder to write code in an exploratory manner
1. Add a test
2. Run the Tests
3. Make a little change
4. Run the Tests
5. Repeat from Step 1
Frameworks have a lot of commonalities. If you learn Mocha or Chai or Jasmine, you can easily pick up another. It's more important to understand what makes a good test.
You can like AT without doing TDD!
Write your tests to be as stupid as possible.
Remember, we want our tests to be as easy to reason about as possible! We don't want to have to test our tests! Keep it straightforward.
- Tests must be isolated.
- Test as small of a thing as you can at once.
- Do not make tests dependent on each other
- Reduce moving pieces
- Reduce state – for example, don't make your test depend on the state or outcome of a previous test.
Stubs/Fakes: Making a function that varies and making it predictable.
Designing:
- High level behavior: such as what should my return value be?
- Make it fail. Always know how your test can fail and see it fail.
- Behavior: you may need to use edge cases to find good ways to test your code. Or, you may need to use a stub/fake.
If you don't need DB persistence, use .build() in Sequelize rather than .create().
These are examples of methods that allow us to better isolate tests through set up and tear down.
Methods like these allow us to create a isolate environment for each test.
Before is run before all of our tests. beforeEach, is run before all of our
it
s within adescribe
block.
Usually, you keep alternating:
- Write one test.
- Write code to pass it.
it
tells us that there will be an expectation. it
s will be run sequentially.
describe
is a useful organizational tool for bundling your tests together. Our test results will take our describe
s and then our it
to create a path/message to the test block.
Usually the describe hierarchy matches the hierarchy of the code.
With done()
When we are testing something async, we must pass in done
to our it
block. Then we invoke done()
after our expect.
done
is always passed into your it, so you can name it anything you want!
If we have an error, we must catch()
it, as with Promises, and then call done()
after our error.
With a promise We can actually put our Mocha/Chai promise into a promise that we create in our tests!
Then, using a promise chain, we can run our test only after the Promise is resolved.
After our promises are done we return
them. Mocha then evaluates that promise for pass/fail.