So you’ve written (or want to write) an awesome Sass toolkit, framework, or even a small function or mixin, and you want to share it with the world. Fantastic! That’s what’s great about open-source software development — you get to collaborate on code with other supportive developers in the community to create useful software that many can benefit from.
But, not so fast! It’s very important to test your software before releasing it. One of the most efficient ways to do this is with unit tests, which are automated scripts that test if a single function of a single unit of code behaves according to a defined specification.
Sass gives you the ability to manipulate data using functions, operators, variables, and control directives and expressions. The return value of a @function
can also be tested directly against the expected value. For CSS output and visual regression testing, a tool like PhantomCSS is better suited for the job, though @mixins
can be tested in the latest version of True.
We will be using Eric Suzanne’s True testing framework for unit testing sample functions in Sass, but the guidelines presented are applicable for any Sass testing framework. For simple tests, you might want to use a minimalistic (and awesome) Sass testing framework such as Hugo Giraudel’s SassyTester.
Setting Up
True can be installed as a Ruby gem (gem install true
) or a Bower package (bower install true
). Configuration options can be found in True’s documentation. From there, a load path can be setup to point to the True installation.
Keep test files in a separate directory from your Sass files. This is so that tests can be easily .gitignore
-d and exempt from accidental Sass watching and compiling. If a project is mainly a Sass project, the directory can simply be named /tests
, with the main test file located at /tests/tests.scss
.
Inside the /tests
directory, it helps to organize your tests by module, which is usually by the @function
or @mixin
it represents. Sometimes, similar functions and mixins can be grouped in the same module.
// Sample Sass project structure
/css
/scss
/tests
/api
_foo.scss
_bar.scss
_all.scss // imports foo and bar tests
/helpers
_baz.scss
_qux.scss
_all.scss // imports baz and qux tests
tests.scss // imports api/_all and helpers/_all
index.html
package.json
// ... etc.
The main test file, tests.scss
, will be responsible for importing and reporting all of the tests and the actual SCSS project that will be tested:
@import 'true';
// Import the project
@import '../scss/project';
// Import the test files
@import 'api/all';
@import 'helpers/all';
// Run the tests
@include report();
Testing Functions
In order to effectively unit test functions, it’s best to collect the specifications of the function being tested. In a modular architecture, each function has a single responsibility — it has one job, and usually returns the same type of value.
A simple way to create specifications for a function is by saying that “it should (do something)” given a certain input. This is a tenet of behavior-driven development, which can be seen in other testing frameworks such as Jasmine (JavaScript).
Let’s take a look at an example function (and relevant variables):
// inside 'scss/project.scss'
$type-base-font-size: 16px !default;
$type-base-ratio: 1.4 !default;
@function type-scale(
$exponent: 1,
$base: $type-base-font-size,
$ratio: $type-base-ratio,
$factor: 1
) {
@return ceil(($factor * $base) * pow($ratio, $exponent));
}
This is a very simple function that should return the appropriate font size based on the modular scale (see type-scale.com for a visual representation of this concept). With this, we have our first spec:
// inside 'tests/api/type-scale.scss'
@include test-module('type-scale') {
@include test('should return the appropriate font size based on the modular scale') {
$actual: type-scale(1);
$expected: 23px; // based on base 16px and ratio 1.4
@include assert-equal($actual, $expected);
}
}
There are three main parts here:
- test-module: the module (or unit) of code that you are testing; in this case, the
type-scale
function - test: a specification or assumption about the unit being tested
- assertion: the assertion that the actual value meets the expected value.
To make tests easier to manage, my preference is to store the $actual
and $expected
values in variables before testing the assertion. In True, there are four types of assertions:
assert-true($value)
asserts that the value is truthyassert-false($value)
asserts that the value is not truthy, e.g.null
orfalse
assert-equal($assert, $expected)
asserts that the actual (asserted) and expected values are equalassert-unequal($assert, $expected)
asserts that the actual (asserted) and (un)expected values are not equal
Let’s go over a few strategies for unit testing functions.
Use a @mixin
for preparing a test, if needed.
In our above function, we’re assuming that the global variables $type-base-font-size == 16px
and $type-base-ratio == 1.4
. Assumptions are dangerous in testing, as is mutable data. Set up a @mixin
that can enforce these values before each test:
@mixin test-type-scale-before() {
$type-base-font-size: 16px !global;
$type-base-ratio: 1.4 !global;
}
Now, these variables will be safe from outside modifications in every test that includes this @mixin
:
@include test-module('type-scale function') {
@include test('...') {
@include test-type-scale-before();
// ... test code
}
}
Test each argument.
A Sass @function
can include many arguments (though it’s best to limit these), and it’s a good idea to test each one individually to ensure that they return the expected result:
@include test('should work with an integer $exponent') {
@include test-type-scale-before();
$actual: type-scale($exponent: 2);
$expected: 32px;
@include assert-equal($actual, $expected);
}
@include test('should work with a negative integer $exponent') {
@include test-type-scale-before();
$actual: type-scale($exponent: -2);
$expected: 9px;
@include assert-equal($actual, $expected);
}
@include test('should work with a numerical font $base') {
@include test-type-scale-before();
$actual: type-scale($exponent: 2, $base: 10px);
$expected: 20px;
@include assert-equal($actual, $expected);
}
// ... etc. for $ratio, $factor
Test the default arguments.
Of course, if your function has default arguments, an assertion should be made that the function, when called with no arguments, returns the expected result.
@include test('should return the base font size with default arguments') {
@include test-type-scale-before();
$actual: type-scale();
$expected: $type-base-font-size;
@include assert-equal($actual, $expected);
}
Iterate through different potential values.
Sometimes, an argument can accept either different types of values, or values with different units. Use the @each
, @while
, or @for
directives to iterate through all potential values the arguments can accept:
@include test('should work with different units for $base') {
@include test-type-scale-before();
$values: (10px, 100%, 10pt); // etc.
$units: ('px', 'percent', 'pt'); // etc.
$expected-values: (14px, 140%, 14pt); // etc.
@for $index from 1 through length($values) {
$value: nth($values, $index);
$unit: nth($units, $index);
$actual-value: type-scale(1, $value);
$expected-value: nth($expected-values, $index);
@include assert-equal($actual-value, $expected-value, 'Works with #{$unit}');
}
}
Testing mixins
With True, a @mixin
can be tested for correctness by comparing the actual output to the expected output. The syntax is similar to the function assertions, with the addition of an @include input
and @include expect
mixin.
Here is an example type-scale mixin that uses the type-scale()
function:
@mixin type-scale($exponent: 0) {
font-size: type-scale($exponent);
}
And here is its accompanying test:
@include test('@mixin type-scale()') {
@include assert('should output the appropriate font size') {
@include input {
@include type-scale(1);
}
@include expect {
font-size: 23px;
}
}
}
Now, when the tests are run, you can compare the .input { ... }
block to the .expect { ... }
block for this test:
/* @mixin type-scale() */
[data-module="type-scale function"] [data-test="@mixin type-scale()"] [data-assert="should output the appropriate font size"] .input {
font-size: 23px;
}
[data-module="type-scale function"] [data-test="@mixin type-scale()"] [data-assert="should output the appropriate font size"] .expect {
font-size: 23px;
}
The guidelines for testing mixins should be the same as those for testing functions. Make sure to test every argument, default values, and iterate through different potential values.
To make mixins easily testable, separate logic out of mixins and into functions. Functions are directly testable, and this will provide greater modularity and separation of concerns inside your project.
Running your tests
With True, there are a couple ways to run the tests – via true-cli
in the terminal, or with node-sass
and your test runner of choice, such as MochaJS. Refer to True’s documentation for more information on setting up and running tests. If you are using the terminal, it’s as simple as running true-cli path/to/your/tests.scss
.
All of the above tests inside @include test()
should be inside an @include test-module()
, with the name of the specific unit you are testing as the name of the module, such as @include test-module('type-scale')
. You can check out this Sassmeister gist to see all the above tests in action.
Final Notes
Unit tests can save you hours of development time by automating the process of testing every part of your project. That way, you can refactor with the reassurance that, if all of the unit tests pass, the code can be safely used and is correctly implemented. Unit testing naturally emphasizes a modular project structure, as well.
A good rule of thumb: less code is better, and more unit tests are better. Keep the scope and responsibilities of your project’s units (mixins and functions) small, and they will be easily testable. Test for every possible input, and ensure that your functions are as idempotent as possible.
If you want to see some real-life usage of unit tests in a Sass library, check out the unit tests I’ve written for Sassdash. Happy testing!
David Khourshid is a front-end web developer and speaker in Orlando, Florida. He is passionate about JavaScript, Sass, and cutting-edge front-end technologies. He is also a pianist and enjoys mathematics, and is constantly finding new ways to apply both math and music theory to web development.