Testing a Sass Function in 5 Minutes
The other day, I was playing with the include-media library from Eduardo Bouças and I wanted to quickly test a function I built so I started writing a little mixin to help me test a lot of different scenarios. After a couple of minutes, I came up with the most minimalistic Sass testing engine you could possibly get.
While this article might be slightly technical, I believe it can be helpful to many people as testing it should be the responsibility of every developer. Also, when going through things one after the other, you’ll see that it actually is not that difficult to understand.
Creating a dummy function to test
Everything starts with a function to test. For our purpose, I suggest we go with a very simple function. Let’s say, a function to double a number.
@function double($value) {
@return $value * 2;
}
Sounds simple enough. Although, and only for our demo concerns, we are going to voluntarily introduce a bug inside our function so that we can actually see that one of our tests fails.
@function double($value) {
// Voluntarily introduced bug for demonstration purpose
@if $value == 3 {
@return 5;
}
@return $value * 2;
}
Writing tests
You might find it surprising, but writing tests in our system is as simple as writing a Sass map where keys are function inputs, and values are expected results.
$tests-double: (
1: 2,
2: 4,
3: 6,
4: 8
);
That’s it! We’ve written our tests. Again: on the left side are the inputs, and on the right side the expected outputs.
The test runner
So far, so good. We have built our function and we have written our tests. We now only need to create the test runner.
If you are familiar with Sass, you might already understand where this is going. Our test runner is going to iterate on the test map, calling the function for each input and making sure that it matches the expected output. Then, it will print the result of our tests.
Here is what our test runner looks like:
/// Run a function ($function) on a test suite ($tests)
/// @param {Map} $tests - Test suite
/// @param {String} $function - Name of function to test
@mixin run-tests($tests, $function) { .. }
Alright. Let’s dig into the belly of the beast. The idea is to build a string with the result of each test, and once everything has been done, print the string with the @error
directive. We could also pass it to the content
property of a pseudo-element for instance, but it’s slightly more complex so we’ll stick to @error
.
First thing to do is to iterate on the test suite. For each test, we dynamically call the function from its name (with the call(..)
function) and we check if the result is as expected.
@mixin run-tests($tests, $function) {
$output: '';
@each $test, $expected-result in $tests {
$result: call($function, $test...);
@if $result == $expected-result {
// Test passed
$output: $output + 'Test passed; ';
} @else {
// Test failed
$output: $output + 'Test failed; ';
}
}
// Print output
@error $output;
}
At this point, we have our system working. Let’s run it on our test suite to see what it looks like.
@include run-tests($tests-double);
Test passed; Test passed; Test failed; Test passed;
Hey! That’s a start right? Now we only need to make the output a bit more helpful (and friendly).
Pimping the output
This is the moment where you can customize the output to make it look like you want. There is no single way to do this, you can output whatever you prefer. Note that as per the CSS specification, you can have a line break in your string using \a
.
In my case, here is what I went with:
@mixin run-tests($tests, $function) {
$output: '';
$length: length($tests);
$failing: 0;
@each $test, $expected-result in $tests {
$result: call($function, $test...);
$test-index: index(map-keys($tests), $test);
$output: $output + '\a Test #{$test-index} out of #{$length}... ';
@if $result == $expected-result {
// Test passed
$output: $output + '✔';
} @else {
// Test failed
$failing: $failing + 1;
$output: $output + '✘\a Expected : `#{$expected-result}`.\a Actual : `#{$result}`.';
}
}
// Print output
@error 'Started tests for function `#{$function}`\a '
+ '-----------------------------------'
+ $output + '\a '
+ '-----------------------------------\a '
+ 'Over: #{$length - $pass} test(s) out of #{$length} failing.';
}
If we run it again over on our $tests-double
for the double
function, here is what we got:
Started tests for function `double`
-----------------------------------
Test 1 out of 4... ✔
Test 2 out of 4... ✔
Test 3 out of 4... ✘
Expected : `6`.
Actual : `5`.
Test 4 out of 4... ✔
-----------------------------------
Over: 1 test(s) out of 4 failing.
Now that’s pretty neat, isn’t it?
Testing functions with multiple arguments
In our example, our function had a single argument but we can rely on the fact that Sass maps accepts anything as keys (including lists) to test functions with several parameters as well. It would look like this:
@function my-function($string, $color, $length) { .. }
$tests-my-function: (
('a', red, 42px): 'My expected result',
// ...
);
If you have a look back at our mixin, you’ll see that we add an ellipsis (...
) to the $test
variable when calling the function with call(..)
.
$result: call($function, $test...);
This means we pass the value $test
as an arglist
. In other words, if $test
is a list (e.g. ('a', red, 42px)
), then it will be passed as several arguments instead as a list.
Final thoughts
There you have it, folks: the most minimalistic Sass testing engine ever. This tiny testing system can come in very handy to test the few functions you could have in your project, especially if you plan on providing it to other developers (framework, library…). Also, I found out it is extremely convenient to quickly test a function on SassMeister. Just drop the mixin and your function there, and run your tests!
Of course if you are looking for more in-depth solution, you might want to have a look at True by Eric Suzanne. As a full testing framework for Sass, it is more suited for global unit test infrastructure.
If you want to have a look at (a slightly more advanced version of) the code, I opened the SassyTester repository to gather everything.
So what do you think?