We finally decided it is probably time to try to migrate to jasmine 2.9.1 from 2.3.4
There is an error that started occurring randomly and before digging down and investigating it and and the end finding out that it is probably a result of a wrong version, we decided to try to get up to date with jasmine.
Latest jasmine version is 3.X, but 2.9.1 is a huge step from 2.3.4
We will try to migrate to 2.9.1 first. The issue is that the moment we migrated there is an error
'beforeEach' should only be used in 'describe' function
It took a couple of minutes, but what we found out is that fixtures are used in different ways.
Here is the difference and what should be done.
jasmine 2.3.4
fixture.set could be in the beforeEach and in the describe
// This works
// fixture.set is in the describe
describe("feature 1", function() {
fixture.set(`<div id="the-div"></div>`);
beforeEach(function() {
})
})
// This works
// fixture.set is in the beforeEach
describe("feature 1", function() {
beforeEach(function() {
fixture.set(`<div id="the-div"></div>`);
})
})
jasmine 2.9.1
fixture.set could be only in the describe and not in the before beforeEach
// This does not work as the fixture is in the beforeEach
describe("feature 1", function() {
beforeEach(function() {
fixture.set(`<div id="the-div"></div>`);
})
})
// This does work
// fixture.set could be only in the describe
describe("feature 1", function() {
fixture.set(`<div id="the-div"></div>`);
beforeEach(function() {
})
})
(Everyday Code – instead of keeping our knowledge in a README.md let’s share it with the internet)
We can all agree that dead code should be removed. What we sometimes fail to see is how much it could cost to leave dead code with our product. Today’s article is about an example of a “dead code” and how it hit us back 1.5 years after we stopped using it. It took us 3 hours to debug and find the root cause. The conclusion is that we should have done ‘git rm’ an year ago. We did not and the cost was 3 hours.
This is a real life example article from our code base and it is design to share experience with our team, but I hope the whole community could benefit.
What happened
1. Jenkins build failed
First time we saw it we identified that it is not something serious and it kept failing for a few days until we had time to address it in the next Sprint.
2. Teaspoon, Jasmine, Bundler, Rails Engines
In the project there was the production code in Rails.root along with a dummy test code for starting JavaScript specs in Rails.root/test/dummy/. We are using Teaspoon with Jasmine in a Rails engines. Rails engines tend to create a test/dummy folder in which you could put the code for a test app in which you want your JavaScript specs to be executed. The product code is loaded in the test app and specs are executed.
About 1.5 years ago I did some experiments with this test app and made a few configurations. The goal was to have the production code along with some test app code both loaded in the test environment. In this way Teaspoon and Jasmine would see both the production and the test app code as coming from “production”. I literally remembered what my idea was and what I was trying to achieve. At the end I decided not to use the test app code, but to have only the production code located in Rails.root be loaded in the test environment.
But I forgot to remove the test app code from test/dummy/app/assets/javascripts/
3. Teaspoon Error
The following error occurred. Here “IS” is a JavaScript namespace part of the production code.
jenkins@vpszap6s:~/jobs/is-core Build and Release/workspace/test/dummy$ xvfb-run -a bundle exec rake teaspoon --verbose
Starting the Teaspoon server...
Teaspoon running default suite at http://127.0.0.1:36051/teaspoon/default
ReferenceError: IS is not defined
4. The error and debugging process
Not sure what the error was exactly I tried to identify the difference between my machine and the server machine where the tests was failing. This server machine is called “elvis”.
Bundler version – my version was 2.2.11 and the elvis version was 2.2.14. Tried with upgrade, but it did not resolve the issue.
Chrome driver – since the tests were executed on chrome I saw the chrome driver versions were different. Mine was 86. elvis was with 89. Synced them, the error was still occurring.
Rake version – rake versions were the same
Ruby version – the same
Teaspoon and Jasmine versions – the same
The OS is the same
I could not find any difference between my env and the elvis env.
Turns out the problem was in the loading order. elvis was loading test app code and then production code. My machine was loading production code and then test app code. I could not figure out why. The whole test app code was:
I was curious to continue debugging to see why the two machines were loading the code in a different order, but at the end decided against it. Just removed the dead code in test/dummy/app/assets/javascripts/ext/dummy/dummy.js which I only thought as dead, but it turned out it was affecting our project.
6. Builds passing
Finally the builds were passing.
Conclusion
Dead code my not be that dead after all. It might be loaded even if not used and loading order could differ. Better be safe and remove dead code at all. If we need it that much, we have it in the GIT repo history.
(Everyday Code – instead of keeping our knowledge in a README.md let’s share it with the internet)
We are serious about specs. A good suite of specs has helped us many times especially when it comes to regressions. For the JavaScript part of our stack many of our specs and using Jasmine and ran with Teaspoon in the Browser environment. This article is about how we use Teaspoon and Jasmine to run our specs for pure JavaScript frameworks like Instructions Steps, and BABYLON.js. Both BuildIn3D and FLLCasts are powered by this specs, but we use them mostly for the Instructions Steps Framework.
Why JavaScript specs?
In the Rails community specs are important. In the JavaScript community – well, they are not that well respected. We can debate the reasons, but this is our observation.
When we implement specs for the platforms we like to use Rails system specs. They give us exactly what we need. Let’s take a real life example.
Filtering by category
We visit the page at BuildIn3D for all the 3D models & building instructions. We filter them by brand. We click on a brand and see only the 3D models & instructions for this specific brand.
Here is the spec. Short and sweet.
scenario "/instructions shows the instructions and can be filted by category", js: true do
# go to the third page as the materials are surely not there
visit "/instructions?page=3"
expect(page).not_to have_link material1.title
click_category category1
# We make sure the url is correct
expect(page).to have_current_path(/\/instructions\?in_categories%5B%5D=#{category1.id}$/, url: true)
# We make sure that only material1 is shown and material2 is not shown
# The materials are filtered by category
expect(page).to have_link material1.title
expect(page).not_to have_link material2.title
endT
The spec is a Rails system spec. Other articles enter into more details about them. The point is:
With a Rails system spec we don’t concern ourselves with JavaScript.
We visit the page, we click, we see that something has changed on the page – like new materials were shown and others were hidden.
What’s the use case for JavaScript specs then?
Take a look at the following embedded BuildIn3D instruction. It is coming live and it has a next button. Click on the next button.
Here is the actual spec in JavaScript with Jasmine and ran everyday with Teaspoon.
it("right arrow button triggers TriedMoveIterator with 1", function(done) {
// This is a JavaScript spec that is inside the browser. It has access to all the APIs
// of the browser.
const eventRight = new KeyboardEvent("keydown", { key: "ArrowRight" });
document.onkeydown(eventRight);
// Wait for a specific event to occur in the Instructions Steps (IS) framework
// We are inside the browser here. There is no communication with the server
IS.EventsUtil.WaitFor(() => this.iteratorListener.event).then(() => {
expect(this.iteratorListener.event.getSteps()).toEqual(1);
done();
});
});
This the how we use JavaScript and this is why we need them:
We need JavaScript specs that are run inside the browser to communicate with other JavaScript objects and the browser APIs for JavaScript apps.
In this specific case there is this “iteratorListener” that monitors how the user follows the instructions. We need access to it. Otherwise it gets quite difficult to test. We can not write a spec to test what are the pixels on the screen that are drawn after clicking next. This will be … difficult to put it mildly. We don’t have to do it. We need to know that clicking the next button has triggered the proper action which will then draw the actual Geometry and Colors on the screen.
How we use Jasmine and Teaspoon to run the JavaScript specs
Years ago I found a tool called Teaspoon. Looking back this has been one of the most useful tools we’ve used in our stack. It allows us to have a Rails project (and we have a lot of those) and to run JavaScript specs in this Rails project. The best thing – it just works (the Rails way).
# add teaspoon dependency
$ cd test/dummy/
$ rails generate teaspoon:install
# write a few specs
$ rails s -p 8889
You start a server, visit the url in the browser and the specs are executed
Tests are pure JavaScript and we use Jasmine.
That’s it. Not much to add. It’s simple The specs are a simple JS file located in “spec/javascripts/” folder and here is an example for one of them
describe("IS.EventDef", function() {
describe("constructing throws error if", function() {
it("event name is null", function() {
expect(() => new IS.EventDef(null, {})).toThrow(new Error("Null argument passed."));
});
it("event conf is null", function() {
expect(() => new IS.EventDef("smoe", null)).toThrow(new Error("Null argument passed."));
});
it("declaring an event, but the class attribute value is null", function() {
expect(() => {
const ext = IS.ExtensionDef.CreateByExtensionConf({
extension: new IS.Extension(),
events: {
declaredEvent: {
class: null
}
}
});
}).toThrow("Declared event 'declaredEvent' class attribute is null!");
});
});
});
Spend some time and resources to set up your Continuous Integration infrastructure to run your spec suites against nightly releases of your dependencies. The benefits are larger than the costs.
Context
To further explain the point I will use an example from today.
We run our specs daily against the latest nightly release of BABYLON.js. On Friday one spec failed. I reported in the forum (not even a github issue). A few hours later there was a fix and PR merged with the main branch of BABYLON.js. We would have the new nightly in a day or two.
Our specs pass with version 4.2.0 of BABYLON.js, but they fail with BABYLON 5.0.0-alpha.6. A few of the hundred of extensions running in the Instructions Steps (IS) Framework are using BABYLON.js. The IS framework is powering the 3D building instructions at FLLCasts and BuildIn3D.
BABYLON.js provides two releases of their library.
How do we run the specs against the preview (nightly) release of BABYLON.js?
We’ve configured Jenkins to do two builds. One is against the official release of BABYLON.js that we are using on production. The second run is against the preview release.
When there is a problem in our code both builds will fail. When there is an issue with the new version of BABYLON.js only the second build fails.
What is the benefit?
I think of the benefit as “being in the context’. Babylon team is working on a new feature or they are changing something. If we find an issue with this change six months later it would be much more difficult for them to switch context and resolve it. Probably there are already other changes. But when we as developers are in the “context”, when we are working on something and have made a change today and there is an issue with this change it is much easier to see where the problem is. You are in the same context.
The other large benefit is that when 5.0.0 is released we will know from day one that we support it and we can switch production to the new version. There are exactly 0 days for us to “migrate” to the new version.
How much does it cost us?
Basically – zero. The specs are run in under 60 seconds and the build is configured with a param.
What if there are API changes?
Yes, we can’t just run the same code if there are API changes in BABYLON.js. That’s why we have the branch. If there are API changes we can change our code in the babylon-5.0 branch and keep it up to date with changes in dev, which is most of the time resolved with a simple merge.
But BABYLON.js is a stable library. There are not many API changes that are happening. At least not in the API that we are using.
Reply
You must be logged in to post a comment.