The State of Vue.js Report 2025 is now available! Case studies, key trends and community insights.
Table of Contents
TL;DR
We have prepared a sample app with nothing more than the necessary configuration and a single, oversimplified spec file. You can find it on github now or first you can get more details about it below.
Background
Karma is a test runner designed for the purpose of AngularJS unit testing. It works with Jasmine, Mocha and other players but configuring it to work nicely inside a Rails environment and with sprockets is a bit tricky. In this post I will show you how we set it up based on our Angular with Rails marriage setup.
We base our setup mostly on the Angular + Rails with no fuss article by Sebastien Saunier which is another good read on the topic.
Requirements
First we need to setup a list of needed npm packages:
{ "name": "app", "dependencies": { "karma": ">= 0.12.16", "karma-jasmine": ">= 0.2.2", "karma-coffee-preprocessor": ">= 0.2.1", "karma-slim-preprocessor": ">= 0.0.1", "karma-phantomjs-launcher": ">= 0.1.4", "karma-ng-html2js-preprocessor": "git://github.com/monterail/karma-ng-html2js-preprocessor#feature/strip-sufix" } }
I think karma, karma-jasmine and karma-phantomjs-launcher don't need any explanation. Karma-coffee-preprocessor and karma-slim-preprocessor will help us to compile coffee and slim templates during the testing phase. The most interesting things happen in karma-ng-html2js-preprocessor which compiles templates on the fly and keeps them in cache, but I will describe that in more detail later.
Karma config
We keep karma configuration under spec/karma/config/unit.js
and it's almost the same as the original one generated by karma itself:
module.exports = function(config) {
config.set({
// base path
basePath: '../',
// frameworks to use
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
APPLICATION_SPEC,
'app/assets/templates/**/*.html.slim',
'spec/javascripts/**/*_spec.{coffee,js}'
],
// list of files to exclude
exclude: [],
// test results reporter to use
// possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera (has to be installed with `npm install karma-opera-launcher`)
// - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`)
// - PhantomJS
// - IE (only Windows; has to be installed with `npm install karma-ie-launcher`)
browsers: ['PhantomJS'],
// If browser does not capture in given timeout [ms], kill it
captureTimeout: 60000,
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: false,
// Preprocessors
preprocessors: {
'**/*.coffee': ['coffee'],
'**/*.slim': ['slim', 'ng-html2js']
},
ngHtml2JsPreprocessor: {
stripPrefix: 'app/assets/templates/',
stripSufix: '.slim'
}
});
};
Now, let's discuss the most significant places:
basePath: '../',
When we run our specs we will build final configuration in tmp
directory so ../
leads to the root path of our application.
files: [
APPLICATION_SPEC,
'app/assets/templates/**/*.slim',
'spec/javascripts/**/*_spec.{coffee,js}'
]
APPLICATION_SPEC
will be replaced with a list of our JavaScript assets managed by sprockets. Next we'll find every *.html.slim
file under assets/templates
and the final line will look for specs.
preprocessors: {
'**/*.coffee': ['coffee'],
'**/*.slim': ['slim', 'ng-html2js']
}
Here we're saying that we want to pass *.coffee
files to coffee-script preprocessor and *.slim
templates through slim
preprocessor and then, parsed to plain html, to ng-html2js
preprocessor which has its own, additional configuration:
ngHtml2JsPreprocessor: {
stripPrefix: 'app/assets/templates/',
stripSufix: '.slim'
}
Karma doesn't allow us to make any non-mocked http requests in unit tests including GET
requests for templates. ng-html2js
will keep templates in $templateCache but we need to remove app/assets/templates
prefix from the path and .slim
extension so they will be accessible under the same path as with Rails (if you don't use our interceptor you will need to add prependPrefix: '/assets/'
too).
Note about templates
Unfortunately ng-html2js
doesn't allow us to stripSufix
(at least - yet) so we had to prepare our own PR for this. Until it is merged, you can use our fork. To use slim in templates you need to create this initializer:
Rails.application.assets.register_mime_type 'text/html', '.html'
Rails.application.assets.register_engine '.slim', Slim::Template
Final steps
The last thing that we need is a way to run karma with sprockets assets injected. Here is a rake task to make this possible:
namespace :karma do
task :start => :environment do
with_tmp_config :start
end
task :run => :environment do
exit with_tmp_config :start, "--single-run"
end
private
def with_tmp_config(command, args = nil)
Tempfile.open('karma_unit.js', Rails.root.join('tmp')) do |f|
f.write unit_js(application_spec_files)
f.flush
system "./node_modules/karma/bin/karma #{command} #{f.path} #{args}"
end
end
def application_spec_files
Rails.application.assets.find_asset("application_spec.js").to_a.map {|e| e.pathname.to_s }
end
def unit_js(files)
unit_js = File.open('spec/karma/config/unit.js', 'r').read
unit_js.gsub "APPLICATION_SPEC", ""#{files.join("","")}""
end
end
It will provide two commands: karma:run
will run all the specs once and karma:start
will run them after each change in specs. Both will use a temporary file as config generated by mixing our unit.js
with the list of assets read with sprockets from application_spec.js
:
// spec/karma/application_spec.js
//= require application
//= require angular-mocks
Spec itself
In our example repo, you can find the simplest possible directive which renders a list of countries. Here is our pseudo-spec:
describe 'Countries Directive', ->
$compile = $scope = $httpBackend = element = null
beforeEach ->
shared = angular.module('shared')
shared.constant('Rails', { env: 'test', templates: {} })
module('application', 'countries.html')
beforeEach(inject((_$compile_, _$rootScope_, _$httpBackend_) ->
$scope = _$rootScope_
$compile = _$compile_
$httpBackend = _$httpBackend_
))
beforeEach ->
$scope.countries = [{
code: 'PL',
name: 'Poland'
}, {
code: 'UK',
name: 'United Kingdom'
}, {
code: 'IT',
name: 'Italy'
}]
element = $compile('<countries data="countries"></countries>')($scope)
$scope.$digest()
it 'renders list of countries', ->
expect(element.html()).toContain('Poland')
And again, let's discuss this step by step.
beforeEach ->
shared = angular.module('shared')
shared.constant('Rails', { env: 'test', templates: {} })
module('application', 'countries.html')
Our first beforeEach
block mocks two things: the shared module with templates hash for interceptor (we won't use it here so it is just empty) and angular-mocks module which registers our application module and template which we use in our directive (remember ng-html2js
? now it loads the template into cache).
beforeEach(inject((_$compile_, _$rootScope_, _$httpBackend_) ->
$scope = _$rootScope_
$compile = _$compile_
$httpBackend = _$httpBackend_
))
Inject, another angular-mock feature, will give us access to any module from the angular which we would need to setup specs.
beforeEach ->
$scope.countries = [{
code: 'PL',
name: 'Poland'
}, {
code: 'UK',
name: 'United Kingdom'
}, {
code: 'IT',
name: 'Italy'
}]
element = $compile('<countries data="countries"></countries>')($scope)
$scope.$digest()
And the final beforeEach
will set mock data into $scope
and compile a mock template on which our directive should be run. The spec itself is self-explanatory.
Bonus
If you use circle CI you can easily bind our karma rake runner there - just add test post
to the circle.yml
and make sure that tmp
(for final config) will be there:
checkout:
post:
- mkdir -p tmp
test:
post:
- bundle exec rake karma:run
For travis it should look similar.
Angular Tips recently published a series of introductory articles on how to write Unit Tests for Angular: services, controllers, directives and some conclusions.
Happy coding!