Here are links to lessons that should be completed before this lesson:
Learn a commonly used testing tool.
Jasmine is a Behavior-Driven Development testing framework for JavaScript. It does not rely on browsers, DOM, or any JavaScript framework. Thus it's suited for websites, Node.js projects, or anywhere that JavaScript can run.(stackshare.io)
Which companies use Jasmine testing?
Participants will be able to:
- Create a testing structure with Jasmine
- Create assertion functions
- Generate, display and watch tests
- Jasmine
- install Jasmine
- create tests using
expect
& matchers liketoBe
,toContain
, andtoBeDefined
Jasmine is a behavior-driven development (BDD) framework for testing JavaScript code. (Note: BDD is a specific style of testing that tests the behavior of the code from the user's perspective, rather than testing implementation details. It is often used with TDD! Learn more.) Jasmine has no external dependencies and does not require a DOM, which means that it's good for getting tests up and running quickly.
As we learned in the last lesson on test-driven development (TDD), one way to ensure that your code is well-tested is to start by writing a test for the behavior you want, watch it fail, and finally write the code to make it pass (the Red-Green-Refactor pattern). Though working in a TDD style may feel slower at first, it can save you time in the long run by ensuring that your code won't break. We'll be working in a TDD style through this lesson.
Let's get started by setting up a new project with Jasmine tests.
Create a new project
mkdir jasmine-practice
- create a folder for your new projectcd jasmine-practice
- change directories to that folder
Install Jasmine
- Install Jasmine using:
npm install --global jasmine
- Note: we are installing Jasmine globally because there is little setup and it's a fast way to get tests up and running. However, when working on bigger projects, prefer a local installation so that when others clone the repository, they will be using the same version of Jasmine that you used (this is not guaranteed with a global installation). We'll learn more about this later.
Initialize Project
- Run
jasmine init
- Notice that initializing Jasmine created a
/spec
directory in your project! This is where your tests will go.
Create Files
- To add tests, create files in the
/spec
folder that end with ".spec.js" so that Jasmine knows which files are the test files. - Create a file that we'll add some tests to:
spec/string.spec.js
. - Note: we named our file
string.spec.js
, because we'll be testing some string functionality! In general, try to name your test files for the behavior that they're testing.
Start Test
- Run
jasmine
from the command line to run your tests! Since we haven't added any tests yet, you'll see something like this:
Started
No specs found
Finished in 0.004 seconds
Incomplete: No specs found
- Congrats! We're all set up to write some tests.
Jasmine Syntax
- Let's look at a complete example. Add the following code to your
string.spec.js
file:
describe('A string', function () {
it('containing 4 letters should have length 4', function () {
WORD = 'word';
expect(WORD.length == 4).toBe(true);
});
});
describe
, provides context for a group of tests.describe
is a function that takes 2 arguments ("STRING", FUNCTION(){}). The "STRING" should describe the context for what we are testing, and the "FUNCTION" will contain one or more tests.- Inside the
describe
, you can add multipleit
statements. Eachit
will contain tests for a specific behavior (also known as "specs"). - To add an actual test, we add an
expect
statement within theit
.- In the above example, we are testing that the length of the string "word" is 4.
- This test passess, because
"word".length == 4
evaluates totrue
!
- Notice that if you read the
describe
andit
statements together, they form the sentence "A string that contains 4 letters should have length 4". It's good practice to write Jasmine tests that read like sentences and clearly state what they are trying to test. - Now, run the new test by typing in
jasmine
in the command line outside of your spec folder. You should see something like this:
Started
.
1 spec, 0 failures
Finished in 0.006 seconds
- This means that all your specs are passing. Hooray!
- To see what Jasmine prints when a test fails, try changing
WORD
to something with 5 letters, e.g.WORD = "words"
. You'll now see something like this:
Started
F
Failures:
1) A string containing 4 letters should have length 4
Message:
Expected false to be true.
Stack:
Error: Expected false to be true.
at <Jasmine>
at UserContext.<anonymous> (/path_to_project/jasmine-practice/spec/string.spec.js:4:34)
at <Jasmine>
1 spec, 1 failure
- We now see the failing test printed to the terminal. Notice that the error message "Expected false to be true" isn't super helpful yet - we'll make the error message more helpful later by using more specific matchers than
toBe
.
Adding more tests
- Right now, we only have one spec in our file. Let's add another by adding an additional
it
statement.
describe('A string', function () {
it('containing 4 letters should have length 4', function () {
WORD = 'word';
expect(WORD.length == 4).toBe(true);
});
// New spec!
it('should be equal to an identical string', function () {
WORD = 'word';
expect(WORD == 'word').toBe(true);
});
});
- When you run the specs again, you should now see 2 specs passing!
- Notice that we used the same value for
WORD
twice. Multipleit
statements can use the same variables if they are declared under thedescribe
scope.
describe('A string', function () {
let WORD = 'word';
it('containing 4 letters should have length 4', function () {
expect(WORD.length == 4).toBe(true);
});
it('should be equal to an identical string', function () {
expect(WORD == 'word').toBe(true);
});
});
- The test now looks a little bit nicer and, should still pass!
Other matchers
- So far, the only matcher that we've looked at is
toBe
, which tests that the actual value in theexpect
evaluates to the expected value. However, Jasmine provides a lot of different matchers that can help us test different behaviors. These matchers can also help by printing out more specific error messages when tests fail. Check out the documentation: https://jasmine.github.io/api/3.5/matchers - Let's add a (failing) test using the
toBeGreaterThan
matcher.
describe('A string', function () {
let WORD = 'word';
// ... previous tests
// New test
it('should be more than 5 characters long', function () {
expect(WORD.length).toBeGreaterThan(5);
});
});
- When you run
jasmine
, we now get the failure message:
Failures:
1) A string should have a length greater than 5
Message:
Expected 4 to be greater than 5.
Stack:
Error: Expected 4 to be greater than 5.
at <Jasmine>
at UserContext.<anonymous> (/Users/brookeangel/Code/jasmine-practice/spec/string.spec.js:13:27)
at <Jasmine>
- Remember our last failing test? This one has a more helpful error message, because it tells us exactly what's wrong: "Expected 4 to be greater than 5." Oops!
WORD.length
is 4 - let's provide a different value so that our test passes:
describe('A string', function () {
// ... previous tests
it('should have a length greater than 5', function () {
expect('elephant'.length).toBeGreaterThan(5);
});
});
- As a general rule, use the matcher that best fits the behavior that you're trying to test. This will provide you with good documentation for the code you're trying to test, as well as helpful error messages.
Including modules in Jasmine tests
- In a typical project, the code that you're testing won't live in the
.spec.js
file, so you'll want to import it modules into your spec file. - Require the packages you need in your test at the top of the spec file. Modules that are being tested need to have a
module.exports
in them.
// src/myFunction.js
function myFunction() {}
module.exports = myFunction;
// spec/myFunction.spec.js
const myFunction = require('../src/myFunction');
Local Installation
- Remember how we installed Jasmine globally? When managing multiple projects and collaborating with others, you should install Jasmine locally on a per-project basis. This ensures that everyone running your project will use the same version of Jasmine. Let's make the following changes to add Jasmine as a local dependency to our practice project:
npm init --yes
- Makes apackage.json
file, for projects that don't already have one.npm install --save-dev jasmine
will save Jasmine locally in your current project. Notice that this creates apackage-lock.json
file in the project. You don't need to understand everything in this file - just know that it specifies exactly which verion of Jasmine you downloaded to the project.- When we initialized Jasmine before, we ran
jasmine init
. With a local installation, you can initialize Jasmine by runningnode node_modules/jasmine/bin/jasmine init
. (We don't have to run this for our project, since Jasmine is already initialized.) - To run Jasmine from the local installation, edit the "test" line in the "package.json" file to say:
"test": "jasmine"
- Now, rather than running
jasmine
from the command line, runnpm test
to run all the specs.
- Remember: you can get "false positives" and "false negatives" in tests. That's why it's good to follow a Red-Green-Refactor pattern, and make sure that your tests fail before implementing the code to make them pass.
- A test with no expectations in it will pass. Don't forget to add at least one
expect
to everyit
function, or you could end up with this false positive. - Pay attention to when you are writing tests for Asynchronous code. The testing engine might complete before asynchronous code has completed running, giving you unreliable tests.
expect
inside of asynchronous code is ignored, therefore passing. (false positive)- Solve this problem in Jasmine with a parameter like
done
- this signals to the test engine this is asynchronous code and it must wait. Learn more.
- Solve this problem in Jasmine with a parameter like
Remember the Basic JavaScript practice that we completed a few lessons ago? We're going to rewrite a few of those functions, TDD style!
FizzBuzz (from basic JS practice, challenge 4)
We're going to TDD a slight twist on fizzBuzz
. The function will:
- Return "fizz" when given a multiple of 3
- Return "buzz" when given a multiple of 5
- Return "fizzbuzz" when given a multiple of 3 and 5
- Otherwise, return the input number
- Within your existing project, create a new test file in the
/spec
directory namedfizzBuzz.spec.js
. Our tests will go here. - Within
fizzBuzz.spec.js
, add adescribe
to add some context. - Let's add our first test: test that
fizzBuzz
is defined.- Hint 1: you'll need to add an
it
within the describe. Make sure it states what you are testing. - Hint 2: there's a new matcher that will help with this -
toBeDefined
. See documentation.
- Hint 1: you'll need to add an
- Run the test using
npm test
. If all goes well, you should see the following failure:
1) fizzBuzz should be defined
Message:
ReferenceError: fizzBuzz is not defined
Check your work so far
// spec/fizzBuzz.spec.js
describe("fizzBuzz", function(){
it("should be defined", function(){
expect(fizzBuzz).toBeDefined();
});
});
Check your work so far
// src/fizzBuzz.js function fizzBuzz() {};
module.exports = fizzBuzz;
// spec/fizzBuzz.spec.js const fizzBuzz = require('../src/fizzBuzz');
describe("fizzBuzz", function(){ it("should be defined", function(){ expect(fizzBuzz).toBeDefined(); }); });
- Great! Our test is passing. Add another test that checks that calling fizzBuzz with a multiple of 3 returns "fizz". Make sure that your test fails.
- Now, make your test pass in the simplest way possible by adding functionality to the
fizzBuzz
function.Check your work so far
// src/fizzBuzz.js function fizzBuzz(num) { return "fizz"; };
module.exports = fizzBuzz;
// spec/fizzBuzz.spec.js
describe("fizzBuzz", function(){
// older tests
it("should return 'fizz' when given a multiple of 3", function(){
expect(fizzBuzz(3)).toBe("fizz");
expect(fizzBuzz(6)).toBe("fizz");
});
});
Notice that we haven't implemented all the functionality for fizzBuzz
yet - we don't have to for the test to pass. That means we should add more tests!
- Let's add the complete functionality for
fizzBuzz
.- Test that when given a multiple of 5, it returns "buzz", then make your test pass.
- Test that when given a multiple of 3 AND 5, it returns "fizzbuzz", then make your test pass.
- Test that when given any other number, it returns the input number, then make your test pass.
Check your work
// src/fizzBuzz.js function fizzBuzz(num) { if (num % 15 === 0) { return "fizzbuzz"; } else if (num % 3 === 0) { return "fizz"; } else if (num % 5 === 0) { return "buzz"; } else { return num; } };
module.exports = fizzBuzz;
// spec/fizzBuzz.spec.js const fizzBuzz = require('../src/fizzBuzz');
describe("fizzBuzz", function(){ it("should be defined", function(){ expect(fizzBuzz).toBeDefined(); });
it("should return 'fizz' when given a multiple of 3", function(){ expect(fizzBuzz(3)).toBe("fizz"); expect(fizzBuzz(6)).toBe("fizz"); });
it("should return 'buzz' when given a multiple of 5", function(){ expect(fizzBuzz(5)).toBe("buzz"); expect(fizzBuzz(10)).toBe("buzz"); });
it("should return 'fizzbuzz' when given a multiple of 3 and 5", function(){ expect(fizzBuzz(15)).toBe("fizzbuzz"); expect(fizzBuzz(30)).toBe("fizzbuzz"); }); });
Exploring new matchers:
- Re-implement Challenge 6, "Sleeping In", from the basic JavaScript practice linked above, using TDD. Since this function returns a boolean, use the matchers
toBeTruthy
andtoBeFalsy
in your tests. - Write a function
mySplit
that takes a string, and returns an array of its letters, using TDD. Use the matchertoContain
in your test.- e.g.
mySplit("dog") => ["d","o","g"]
- e.g.
Challenge 1: Read the docs on beforeEach
and afterEach
. Try to write a test that uses beforeEach
to set up tests. There's a good example in the Jasmine Tutorial.
Challenge 2: You can also run Jasmine tests in the browser for a nicer UI! Follow the installation instructions on Jasmine's github repo to make your tests run in the browser instead of the command line.
- JS testing with Jasmine blog post, link
- Video by Dylan C. Israel Unit Testing in JavaScript and Jasmine starts at 12:00 minutes to show mocha test.
Question: What is Jasmine and how does it work in your project?
Exercise: Each student should pick a matcher, like toBeNull()
. Then describe that matcher to the class and how it should be used.