Some say that if your code doesn’t have tests, you don’t have shippable code. When it comes to CSS or Sass unit testing, there are few tools to choose from.
Today, we’ll explore the testing tool Sass and show you some of it’s top features.
This mini-tutorial will focus on setting up Sass True, the Sass unit testing framework developed by Miriam Suzanne.
Create your own Sass web projects using hands-on instruction.
Sass for CSS: Advanced Frontend Development
Unit testing in Sass is not much different from unit testing in any other language. There are methods to define a test module, a method to wrap a series of tests or assertions, then there are 4 assertion-types: assert-true, assert-false, assert-equal and assert-unequal.
The only other thing to consider is that there are two different patterns to follow for testing. One for functions that will evaluate the output of the function, the other is for mixins which expects a specific return based on the configuration of the mixin.
Before diving deeper into testing, a quick refresher on modern Sass will make this tutorial more complete.
The reference compiler is Dart Sass.
It supports both:
.sass syntax — older and whitespace-based.Most teams default to SCSS because it mirrors CSS and integrates better with modern tooling.
Prefer the @use and @forward system over the deprecated @import.
@use 'tokens/colors' as *;
This brings a module into scope (optionally under an alias) and namespaces variables, mixins, and functions by default to prevent naming collisions.
To re-export a module and build a public API:
@forward 'tokens/colors';
Name partial files with a leading underscore (e.g., _buttons.scss) and import them via modules without the underscore:
@use 'components/buttons';
Namespacing from @use helps avoid hidden globals that make Sass True tests unpredictable.
It also makes test fixtures clearer and more modular — you can import only what a test needs.
If you’ve inherited a codebase that still uses @import, keep it working for now, but migrate toward @use and @forward.
Your tests (and your codebase) will become more reliable, maintainable, and easier to read.
For the sake of clarity, this tutorial is going to assume that there are no files other than the Sass we want to test and the test files.
With a clean directory, you want to run the following installation command:
$ npm i node-sass sass-true glob jest
Now create two directories:
$ mkdir src tests
In your package.json, update to the following:
"scripts": {
"test": "jest"
},
With everything installed, let’s write some code.
A quick note on engines for this Sass tutorial — always prefer Dart Sass over node-sass.
@use, @forward, and the new module system).If your project currently uses node-sass, you can still run Sass True, but expect minor discrepancies in edge cases.
"devDependencies": {
"sass": "^1.77.0", // Dart Sass
"sass-true": "^7.0.0",
"glob": "^10.3.0",
"jest": "^29.0.0"
}
You can exclude node-sass from your Jest pipeline entirely and run only Dart Sass while you migrate.
This ensures consistency across environments and helps you align with the modern Sass ecosystem.
This is the last part of the setup needed to get the jest command working with Sass True. We need to write a small JS shim.
In the tests dir, create the new shim file:
$ touch tests/scss.spec.js
In this new file, add the following code:
const path = require('path')
const sassTrue = require('sass-true')
const glob = require('glob')
describe('Sass', () => {
// Find all of the Sass files that end in `*.spec.scss` in any directory of this project.
// I use path.resolve because True requires absolute paths to compile test files.
const sassTestFiles = glob.sync(path.resolve(process.cwd(), 'tests/**/*.spec.scss'))
// Run True on every file found with the describe and it methods provided
sassTestFiles.forEach(file =>
sassTrue.runSass({ file }, { describe, it })
)
})
This shim is taking the instructions from the Sass True docs and adding some superpowers to make things much easier. Mainly, this is automatically looping through all the spec files so that you don’t have to write out all the absolute paths to the files as Sass True requires.
With this in place, we are ready to start writing tests.
Sass True only will test mixins and functions. After all, that is all you need to ensure that your tooling is always working correctly. Testing the CSS output itself is another tool’s job.
In your project, create the new Sass file that we will use as the function to test.
$ touch src/_map-deep-get.scss
In the file, add the following code:
/// This function is to be used to return nested key values within a nested map
/// @group utility
/// @parameter {Variable} $map [null] - pass in map to be evaluated
/// @parameter {Variable} $keys [null] - pass in keys to be evaluated
/// @link https://css-tricks.com/snippets/sass/deep-getset-maps/ Article from CSS-Tricks
/// @example scss - pass map and strings/variables into function
/// $map: (
/// 'size': (
/// 'sml': 10px
/// )
/// );
/// $var: map-deep-get($tokens, 'size', 'sml'); => 10px
///
@function map-deep-get($map, $keys...) {
@each $key in $keys {
$map: map-get($map, $key);
}
@return $map;
}
This is a pretty common function that is added to Sass projects.
True’s @include assert-equal is perfect for testing functions, but mixins usually emit blocks of CSS — making snapshot-style tests a better fit.
This approach helps your Sass tutorial cover one of the most common real-world team workflows.
// tests/_color-utilities.spec.scss
@use 'true' as *;
@use '../src/color-utilities' as cu;
@include describe('color-utilities') {
@include it('emits utility classes for brand colors') {
@include assert {
@include output {
.text-brand { color: cu.brand('primary'); }
.bg-brand { background-color: cu.brand('primary'); }
}
@include expect {
.text-brand { color: #0a84ff; }
.bg-brand { background-color: #0a84ff; }
}
}
}
}
This pattern keeps your expected CSS adjacent to the test, making results easy to read and maintain.
If your team prefers JavaScript-based testing, you can compile a fixture .scss file to CSS in your .spec.js and compare it to a stored snapshot:
expect(css).toMatchSnapshot();
Snapshot tests are especially useful when mixins output multiple declarations, media queries, or complex selectors.
They make visual and structural regressions easy to spot — a powerful addition to your Sass testing workflow.
Let’s create the new file needed to test the Sass. Remember that in the shim, we are looking for any file that matches *.spec.scss, so to test our map function, we can create a file name like this.
$ touch tests/mapDeep.spec.scss
What’s powerful with Sass True is that it’s written with Sass. Because of this, all the features of Sass are available to you when writing tests.
In this new file, let’s import the dependencies we need. First, we need to import True, then we need to import the Sass function which we want to test.
@import 'true';
@import '../src/map-deep-get';
Looking at the code for the map function, it’s clear that we need a map to use in this test.
$map: (
'size': (
'sml': 10px
)
);
For the test, the first thing needed is to describe the test you are running. This will help you see the results of the test in the CLI output.
To me, the docs were a little hard to follow as they still describe the actual mixins used in Sass True. It appears that for compatibility with testing frameworks like Jest, several aliases were created. I will try my best to address these cross references in the code.
Remember in the shim we have the argument of { describe, it }. In Sass True, for example, @mixin test-module() is aliased as describe() and the test() mixin is aliased to the it() mixin.
@include describe('map-deep-get()') {
...
}
Next, you need to define the test. This will use the it() mixin.
@include describe('map-deep-get()') {
@include it('should return the value from a deep-map request') {
...
}
}
Last we need the test itself. In this example, we will use the asset-equal method.
assert-equal: Assert that two parameters are equal. Assertions are used inside thetest()mixin to define the expected results of the test.
The assert-equal() mixin takes four arguments, but we are only going to use $assert and $expected.
assert-equal($assert, $expected);
Put the function inside the parentheses of the assert-equal() mixin as if we were to use it in Sass. As illustrated, we can also take advantage of the $map variable we set earlier.
ProTip: in your project, if you have a series of variables you are already using and need these available for the test, simply
@importthem before running the test(s).
For the $expected argument of the test, we put in the expected value.
@include describe('map-deep-get()') {
@include it('should return the value from a deep-map request') {
@include assert-equal(
map-deep-get($map, 'size', 'sml'), 10px
);
}
}
At this point, you should have a setup that has all the libraries needed, a starter function to test, and a working test.
At this point, it’s all downhill. From the command line, run your test.
$ npm test
All things working correctly, you should see the following:
PASS tests/scss.spec.js
Sass
map-deep-get()
✓ should return the value from a deep-map request (1ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.422s
Ran all test suites.
At this point, you should have Sass True set up and have the map function test working. Moving forward you may want to test a mixin. Testing a mixin is only slightly different than testing a function. For the mixin test, let’s test a common pattern of creating a breakpoint mixin.
$ touch src/_breakpoint--lg.scss
The mixin itself will look like the following:
/// Standard breakpoint to support resolutions greater than 1232px.
/// @group responsive
/// @example scss - Set breakpoint
/// .breakpoint--lg {
/// @include breakpoint--lg {
/// color: orange;
/// }
/// }
@mixin breakpoint--lg {
@media screen and (min-width: $breakpoint-lg) {
@content;
}
}
In the mixin, you may have noticed that there is the global var $breakpoint-lg as part of the mixin. To make this a little more ‘real-world’, let’s create a global vars file and define the value of this variable.
$ touch src/_globals.scss
And in this file, will put the variable and its value.
$breakpoint-lg: 1232px;
Getting to the test, let’s create the test file.
$ touch tests/breakpoint.spec.scss
To start this file, we will define all our dependencies.
@import 'true';
@import '../src/globals';
@import '../src/breakpoint--lg';
Next, just like the function test, we need to describe this mixin test.
@include describe('breakpoint--lg()') {
...
}
Next, we’ll define what it is expected to do.
@include describe('breakpoint--lg()') {
@include it('should return content within pre-defined media query') {
...
}
}
With the mixin, we can simply use the assert() method for our test.
@include describe('breakpoint--lg()') {
@include it('should return content within pre-defined media query') {
@include assert {
...
}
}
}
Within the assert() method, we want to test the content to be evaluated using the output() method.
output: Describe the test content to be evaluated against the pairedexpect()block. Assertions are used inside thetest()[orit()] mixin to define the expected results of the test.
Ok, so that simply means that within the output() mixin, you can include whatever Sass you want that uses the mixin you want to test. For example, let’s create a selector that uses the breakpoint mixin and inject the @content value inside the mixin.
@include describe('breakpoint--lg()') {
@include it('should return content within pre-defined media query') {
@include assert {
@include output {
.breakpoint--lg {
@include breakpoint--lg {
color: orange;
}
}
}
}
}
}
For the last part, we need an assertion of what the output CSS will be. For this, we will use the expect() method.
expect:Describe the expected results of the pairedoutput()block. Theexpect()mixin requires a content block and should be nested inside theassert()mixin, along with a singleoutput()block. Assertions are used inside thetest()mixin to define the expected results of the test.
So within the expect() mixin, we just add the expected CSS output.
@include describe('breakpoint--lg()') {
@include it('should return content within pre-defined media query') {
@include assert {
@include output {
.breakpoint--lg {
@include breakpoint--lg {
color: orange;
}
}
}
@include expect {
@media screen and (min-width: 1232px) {
.breakpoint--lg {
color: orange;
}
}
}
}
}
}
At this point, you have the Sass test suite with two individual tests running one assertion each.
Running the $ npm test command, you should see the following:
PASS tests/scss.spec.js
Sass
breakpoint--lg()
✓ should return content within pre-defined media query (2ms)
map-deep-get()
✓ should return the value from a deep-map request
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.512s
Ran all test suites.
In today’s world, Sass code is getting more and more complex. Testing will therefore become even more important as time goes on.
To help you prepare for creating advanced Sass projects, Educative has created the course Sass for CSS: Advanced Frontend Development. This course has hands on tutorials and examples for all the top Sass techniques like nesting, variables, mixins, partials, and dynamic functions. By the end, you’ll have all the experience you need to create and ship your own Sass projects.
Happy testing!