For this tutorial I was primarily interested in exploring some testing frameworks in 3 different languages while practicing some basic algorithms. The languages I chose to work in were Ruby, Python and Javascript. They are all general purpose, interpreted untyped languages that share quite a few similarities. The modules I chose to use for testing each framework result in similar syntax - expect or assert statements that check for equality. They all also include library functions for mocking data and doing set up to run the tests. While they each create similar environments, there are differences in set up and differences in what they actually are. RSpec is a Domain Specific Language written in ruby to test ruby code. Unittest is a testing framework for python code that's included as a standard module. Jest is an all-in-1 testing library for javascript code maintained by facebook.
Setting up RSpec is as easy as gem install rspec. This core gem includes rspec-core, rspec-expectations, rspec-mocks and rspec rails. Like most things in ruby there are some expectations for how you set up your file structure. Running the command rspec in the command line will run every file in the "spec" folder ending with _spec.rb. Like so:
>> rspec
23514624000
51267216F.......
Failures:
1) Euler_11#grid_search finds the greatest product 4 in a row in any direction
Failure/Error: expect(example.grid_search).to eq(64)
expected: 64
got: 32
(compared using ==)
# ./spec/euler_11_spec.rb:8:in `block (3 levels) in <top (required)>'
Finished in 6.86 seconds (files took 0.77065 seconds to load)
8 examples, 1 failure
Failed examples:
rspec ./spec/euler_11_spec.rb:7 # Euler_11#grid_search finds the greatest product 4 in a row in any direction
In my case I have 8 passing tests and 1 failed test. Note how it runs the expected operation and lists the output. It can be very helpful in troubleshooting what the actual error is.
Here's an example rspec test:
require_relative 'spec_helper'
RSpec.describe Euler_5 do
let(:example) {Euler_5.new}
describe ".new" do
it "intializes a new object" do
expect(example).to be_a(Euler_5)
end
end
describe "#check_factors" do
it "checks if number is a factor of every number in array" do
expect(example.check_factors(1000)).to equal(false)
end
end
describe "#find_smallest_number" do
it "finds the smallest number that is a factor of factors" do
expect(example.find_smallest_number).to be_a(Integer)
expect(example.find_smallest_number).to_not be_a(String)
end
end
end
In spec helper all of the various modules you are testing and various packages are listed. This is just a convention, you could require the individual files inside the test file. RSpec is typically used to test objects, and the opening RSpec.describe block links this test with the Euler_5 object linked to from the spec_helper. The "let(:example) {Euler_5.new}" is code for setting up the object to be tested and avoid code duplication - example will persist as an instance of Euler_5. Each describe/it block divides the tests into groups and the language will appear in a verbose running of the tests or by default when there's an error. Each describe block typically defines a method and each it statement further sub-divides the method into specific output blocks. The last piece of the test, which is common to every test format I've worked with, are the equality statements, which look similar in every language.
Setting up Jest was the biggest pain in the ass of the three. Installing jest itself wasn't much harder: npm install --save-dev jest. However, I really wanted to use ES6 syntax for imports, which is suppose to be very easy to do... but I couldn't get it to work. I built a package.json that looked like this:
{
"devDependencies": {
"babel-core": "^6.26.3",
"babel-jest": "^23.6.0",
"jest": "^23.6.0",
"regenerator-runtime": "^0.12.1"
},
"scripts": {
"test": "jest"
}
}
Node, which is the runt-time environment for Jest and any non-browser javascript, reads ES5 javascript, but Babel-core and Babel-jest should have made it possible to translate and test ES6 syntax without much trouble. Babel tends to work seamlessly for writing ES6 code... but apparently not for testing it, oh well. The scripts hash runs the commands listed. In this case, npm test, will run "jest", which I actually don't have access to in this set up on the command line... I'm not quite sure how that is working. An example Jest test looks like this:
const {isPalindrome, reverseString, buildArray, main} = require("./palindrome-product")
test("a number is a palindrome", () => {
expect(isPalindrome(1001)).toBe(true)
})
test("reverse string reverses a string", () => {
expect(reverseString("hello")).toBe("olleh")
})
test("buildarray returns an array of products for each number under num and sorts them least to greatest", () => {
expect(buildArray(5)).toEqual([1, 2, 3, 4, 4, 6, 8, 9, 12, 16])
})
test("main finds the greatest palindrome product", () => {
expect(main(100)).toEqual(9009)
})
I used the ES5 import syntax which just generally looks and feels clunky to me... but works. The rest of the test syntax is pretty straight-forward. I didn't need to mock any data in this case so each test case is for each individual module. Test, functions very similarly to ruby describe blocks but works a bit differently, it takes a string and a function that is typically an equality statement as an input, if the equality statement returns true the test passes and if the equality returns false, we get the descriptive string and the results of the equality statement. Here's an output from running npm test:
➜ javascript git:(master) ✗ npm test
> @ test /Users/Nate/compsci-fall-2018/code-testing/javascript
> jest
PASS ./sum.test.js
PASS ./euler_12.test.js
PASS ./palindrome.test.js
● Console
console.log palindrome-product.js:45
906609
PASS ./euler_9.test.js
● Console
console.log euler_9.js:20
31875000
Test Suites: 4 passed, 4 total
Tests: 10 passed, 10 total
Snapshots: 0 total
Time: 2.805s
Ran all test suites.
A failed test would look like this:
>> FAIL ./palindrome.test.js
● Console
console.log palindrome-product.js:45
906609
● reverse string reverses a string
expect(received).toBe(expected) // Object.is equality
Expected: "ollehs"
Received: "olleh"
6 |
7 | test("reverse string reverses a string", () => {
> 8 | expect(reverseString("hello")).toBe("ollehs")
| ^
9 | })
10 |
11 | test("buildarray returns an array of products for each number under num and sorts them least to greatest", () => {
at Object.toBe (palindrome.test.js:8:34)
Unittest took the least amount of setup as it is a standard module included with the language. It's also the most low-level framework. Nowhere do you see plain english it statements. Unittest provides a testCase wrapper that lets you set up groups of test. There's a setup function that lets you set up some objects or data structures to avoid duplication in the tests. The rest of the tests are methods containing simple equality statements that return as failed if they aren't true. Here's an example of a unittest test:
import unittest
from euler6 import Euler6
class Euler_6_test(unittest.TestCase):
def setUp(self):
self.max = Euler6(10)
def test_squaresum(self):
self.assertEqual(385, self.max.squaresum())
def test_sumsquared(self):
self.assertEqual(3025, self.max.sumsqaured())
if __name__ == '__main__':
unittest.main()
There's suppose to be a test-discovery method in which running python -m unittest will initiate a running of all discoverable tests, but I never got that to work. I've just been running them individually like so:
>>➜ python git:(master) ✗ python -m unittest -v euler_2_test
the even sum of even fibonacci numbers under 4000000 is:
4613732
test_add_array (euler_2_test.Euler_2_test) ... FAIL
test_even_number (euler_2_test.Euler_2_test) ... ok
test_fibonacci_array (euler_2_test.Euler_2_test) ... ok
======================================================================
FAIL: test_add_array (euler_2_test.Euler_2_test)
----------------------------------------------------------------------
Traceback (most recent call last):
File "euler_2_test.py", line 18, in test_add_array
self.assertEqual(45, self.array.add_array(even_array))
AssertionError: 45 != 44
----------------------------------------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=1)
The outputs aren't quite as clear without the plain describe and test blocks from RSpec and Jest, but it can still be quite helpful for pointing out errors in your code.
https://github.com/Nate-weeks/compsci-fall-2018/tree/master/code-testing
That's the link to the repot with all the euler problems and tests I've written for them. Below are links to a bunch I submitted throughout the semester. There's 14 in all, though not all of them actually have tests written. I started off doing strict TDD... which felt amusingly unnecessary for simple algorithm problems but was an interesting mental exercise. As the algorithms got a bit more complicated to write I struggled with conceptualizing the tests first and just wanted to dig into problem solving and test/debug later. I found writing some RSpec tests for the 2 dimensional grid navigation problem we were doing to be particularly helpful but often they just aren't necessary for these smaller modules. Regardless, messing with the different frameworks and getting a little more practice with their basic syntax was a useful experience.
last modified | size | ||
euler_1.rb | Tue Dec 03 2024 05:38 pm | 479B | |
euler_1_spec.rb | Tue Dec 03 2024 05:38 pm | 578B | |
euler_1_test.rb | Tue Dec 03 2024 05:38 pm | 271B | |
jim_euler_11_start.py | Tue Dec 03 2024 05:38 pm | 959B | |
spec_helper.rb | Tue Dec 03 2024 05:38 pm | 30B |