Solving the Firehose Javascript challenge Celsius to Fahrenheit with tests.
Before we begin, there is a piece of information missing from the challenge.
When you run npm install --save readline-sync
, you'll likely see an error like this:
npm WARN enoent ENOENT: no such file or directory, open '/Users/chris/Work/theFirehoseProject/JS/package.json'
npm WARN JS No description
npm WARN JS No repository field.
npm WARN JS No README data
npm WARN JS No license field.
This is telling us that the package.json
file, which is standard for any Node.js project, is missing from our project directory.
Luckily, there is an easy way to fix that.
The npm
command itself contains a tool to generate that file for us.
Simply run npm init
, and it will ask you a couple of questions about your project, and then generate the file for you.
NOTE: as an aside, the purpose of the package.json
file in Node.js is similar to that of the Gemfile
in Ruby:
it contains a list of all the packages (gems in Ruby) that need to be present for the program to run.
Unlike Ruby's Gemfile
, however, the package.json
file also contains metadata, i.e. information about the project (such as name, description, version number, name of the author, and homepage). All of these, except name and version number, are optional.
Okay, at this point I'm assuming you've already solved the challenge (not hard!), and your cel2fah.js
looks something like this:
var readlineSync = require('readline-sync');
var degrees = readlineSync.question('Enter degrees in Celsius: ');
var degreesNum = Number(degrees);
var degreesFahrenheit = degreesNum * 1.8 + 32;
console.log('It is ' + degreesFahrenheit + ' degrees Fahrenheit!');
Next, we'll install Mocha.
Thanks to npm
, this is very simple (but different from Ruby): simply run npm install --save-dev mocha
.
This will install Mocha and update the package.json
file for you, recording the Mocha dependency.
NOTE: you may be wondering at this point about the difference between NPM's --save
and --save-dev
flags.
Similar to Gemfiles, where you can define different groups of gems (such as development
and production
), and thus mark them to be installed only in specific environments, NPM has the concept of "runtime dependencies" and "development dependencies".
Basically, runtime dependencies are packages that must be present in order to run the code at all (such as on a server), while development dependencies are packages that only need to be installed in order to work on the project.
Generally, any tool such as test frameworks (mocha), task runners (grunt, gulp), preprocessors (SCSS, Stylus), and so on will be installed as development dependencies.
Before we move on, however, since Mocha is a command line tool, we must also install it globally, so we can run it from the terminal.
Simply run npm install --global mocha
and you should be good to go.
To test if this was successful, type mocha
(followed by enter) at the terminal.
You should see something like this:
Warning: Could not find any test files matching pattern: test
No test files found
Of course, we don't have any tests yet!
Okay, let's start by creating a dummy test just so we can see mocha pick it up and run it.
Create a new directory called test
, and inside, create a file called cel2fah_spec.js
(unlike RSpec, Mocha expects to find tests in the test
directory by default. However, just like RSpec, it still expects them to end with _spec.js
).
For now, we'll just put this in there:
console.log('Hello from Mocha!');
Run mocha again by typing mocha
, followed by enter, and you should see this:
Hello from Mocha!
0 passing (1ms)
So, no more error, our file was obviously found and executed. But there were no test cases in it, so we get 0 passing.
So now, let's actually write a test!
Change the file to look like this:
describe('A test', function() {
it('tests something', function() {
console.log('Hello from Mocha')
});
});
After saving it and running mocha
again, you should see this:
A test
Hello from Mocha!
✓ tests something
1 passing (7ms)
Great! Mocha found our test case, and since it didn't throw an exception, it considers it successful.
Now, this is not a very useful test yet, since it doesn't actually have an assertion (expectation). In other words, it cannot fail, it will always succeed. Let's fix that now:
var assert = require('assert');
describe('A test', function() {
it('checks that 1 + 1 = 2', function() {
assert.equal(1+1, 2)
});
});
Now, let's run mocha
one more time!
A test
✓ checks that 1 + 1 = 2
1 passing (8ms)
So, now that we now how to write tests and run them, we should actually write some tests to check that our code is working properly. However, there is a problem with that: the current version expects to read a value from the keyboard and writes it directly to the screen! In order to test it, we need to be able to pass a value in and get a value out.
So, let's rewrite our code so we can actually test it!
If you take a close look at cel2fah.js
, you notice quickly that all lines but one deal only with the process of reading the input from the terminal and writing it back out.
The only line that actually does any computation is this one:
var degreesFahrenheit = degreesNum * 1.8 + 32;
This is the code we actually want to test!
So, what do we do? We just make it into a function!
function celsiusToFahrenheit(degrees) {
return degrees * 1.8 + 32;
}
So then we could test it like this:
describe('celsiusToFahrenheit', function() {
it('converts 0 Celcius to 32 Fahrenheit', function() {
assert.equal(celsiusToFahrenheit(0), 32)
});
it('converts 30 Celcius to 86 Fahrenheit', function() {
assert.equal(celsiusToFahrenheit(30), 86)
});
});
Okay, but where do we put this function? And how can we access it from our test?
In Ruby, we can use require_relative
to import all the contents of a file. But in Node.js, it doesn't work like that.
Instead of explaining it in theoretical detail, I'll just show you how it's done and let the code speak for itself.
First, just as in Ruby, we'll want all our tested code in a directory called lib
. So let's create lib/cel2fah.js
and put our function there:
module.exports = function celsiusToFahrenheit(degrees) {
return degrees * 1.8 + 32;
}
Note the module.exports
assignment at the beginning.
This is how we tell Node.js what from this file we want to be available to other files who require
this file.
If you don't assign anything to this, require
ing the file will load and execute all the code, but only return an empty object (so none of the code will be callable for another file).
Now, let's import our new code into our test file so we can use it:
var celsiusToFahrenheit = require('../lib/cel2fah.js');
var assert = require('assert');
describe('celsiusToFahrenheit', function() {
it('converts 0 Celcius to 32 Fahrenheit', function() {
assert.equal(32, celsiusToFahrenheit(0))
});
it('converts 30 Celcius to 86 Fahrenheit', function() {
assert.equal(86, celsiusToFahrenheit(30))
});
});
Note that there is no require_relative
. In Node.js, you just use relative paths in require
(which of course you can also do in Ruby, require_relative
is just for convenience).
Also note that generally, anytime you use require
in Node.js, you want to use the return value and assign it to a variable. That's because require
returns whatever the required file assigns to module.exports
. This could be an object, a function, or even just a value.
Let's run the tests to see if it works:
celsiusToFahrenheit
✓ converts 0 Celcius to 32 Fahrenheit
✓ converts 30 Celcius to 86 Fahrenheit
2 passing (7ms)
Now, we also need to change our main cel2fah.js
to use our new file:
var celsiusToFahrenheit = require('./lib/cel2fah.js');
var readlineSync = require('readline-sync');
var degrees = readlineSync.question('Enter degrees in Celsius: ');
var degreesNum = Number(degrees);
var degreesFahrenheit = celsiusToFahrenheit(degreesNum);
console.log('It is ' + degreesFahrenheit + ' degrees Fahrenheit!');
Run node cel2fah.js
again to check that it works. Done!