Lately, I have spent a decent amount of time working with Eduardo Bou?as on include-media. We went through a lot of refactoring so decided to write some tests and run them on every commit to be sure we did not break anything. I’ll go through the details in this article.
If you don’t know include-media yet, it is a very lightweight yet powerful breakpoint manager in Sass.
The provided public API is a single mixin, media(..) (hence the name of the library), but the whole thing is well thought enough so you can actually do wonders with it. A short example before getting started:
<span>.my-component { </span><span> <span>width: 100%;</span> </span> <span>// On screens larger or equal to *small* breakpoint, </span> <span>// make the component floated and half the size </span><span> <span>@include media('≥small') {</span> </span><span> <span>float: left;</span> </span><span> <span>width: 50%;</span> </span> <span>} </span><span>}</span>
Now that’s pretty rad, isn’t it?
Anyway, so we came up with a little testing system that I would like to share with you guys. Of course, if you want to test a full framework, you might want to use True from Eric Suzanne instead, which is a full blown testing framework written in Sass, for Sass and was introduced and explained by David in a recent article on SitePoint.
What’s the idea?
We wanted to run a few tests on the main private functions from the library any time we commit to the repository. If any test fails, the commit is aborted and the code needs to be fixed to allow the commit to pass. This way, we make sure that we can safely work on the library without risking breaking it (which is usually a bad thing).
Achieving something like this ended up being surprisingly easy: we set up a pre-commit Git hook to run tests in both LibSass and Ruby Sass before any commiting. If the test is failing, we kill the process.
There are different ways to run Sass and LibSass. You can either have binaries, or you could use a wrapper. In our case, we opted for a tiny Gulp workflow, making it easy for us to run both Ruby Sass and LibSass.
We wanted something very simple, so tests are written in Sass using SassyTester, which I introduced recently in the article Testing a Sass function in 5 minutes. SassyTester is about 25 lines long. The testing function only outputs a Sass map with the results from the tests. From there, we can do anything we want with it. In our case, we want to throw an error if a test fails. To do so, we have the @error directive from Sass!
When compiling the Sass tests, if the Gulp task encounters a Sass error, it exits the process while throwing an error itself, which bubbles up to the pre-commit hook and finally aborts the commit.
If we sum this up, it goes like this:
- A pre-commit hook runs a test Gulp task on commit
- The test Gulp task compiles Sass tests in both LibSass and Ruby Sass
- If a test fails, Sass throws an error with @error
- The Sass error is caught by Gulp which itself emits an error
- The Gulp error is caught by the pre-commit hook which aborts the commit
So far, so good?
Setting up the testing architecture
The architecture word makes it sound so big while it actually is extremely simple. Here is what the project could look like:
<span>.my-component { </span><span> <span>width: 100%;</span> </span> <span>// On screens larger or equal to *small* breakpoint, </span> <span>// make the component floated and half the size </span><span> <span>@include media('≥small') {</span> </span><span> <span>float: left;</span> </span><span> <span>width: 50%;</span> </span> <span>} </span><span>}</span>
Not that impressive after all, heh? The Gulp task will simply run the Sass engines on all files in the tests folder. Here is what function-1.scss could look like:
dist/ <span>| </span><span>|- my-sass-library.scss </span><span>| </span>tests/ <span>| </span><span>|- helpers/ </span><span>| |- _SassyTester.scss </span><span>| |- _custom-formatter.scss </span><span>| </span><span>|- function-1.scss </span><span>|- function-2.scss </span><span>|- ...</span>
Last but not least, we need to redefine the run(..) because the original one from SassyTester outputs the tests results with @error no matter whether they all pass or not. In our case, we only want to throw if there is an error. Let’s just put it in helpers/_output-formatter.scss.
<span>// Import the library to test (or only the function if you can) </span><span><span>@import '../dist/my-sass-library';</span> </span> <span>// Import the tester </span><span><span>@import 'helpers/SassyTester';</span> </span> <span>// Import the custom formatter </span><span><span>@import 'helpers/custom-formatter';</span> </span> <span>// Write the tests </span><span>// See my previous article to know more about this: </span><span>// http://... </span><span><span>$tests-function-1: ( ... );</span> </span> <span>// Run the tests </span><span><span>@include run(test('function-1', $tests-function-1));</span></span>
For a more advanced version of an equivalent run(..) mixin, check the one from include-media.
The Gulp workflow
If you want a short introduction to Gulp, please be sure to read my recent article about it: A Simple Gulpy Workflow for Sass. For this section, I’ll assume you’re familiar with Gulp.
We need three tasks:
- one to run LibSass on tests folder (using gulp-sass)
- one to run Ruby Sass on tests folder (using gulp-ruby-sass)
- one to run the two previous tasks
<span>// We overwrite the `run(..)` mixin from SassyTester to make it throw </span><span>// an `@error` only if a test fails. The only argument needed by the </span><span>// `run(..)` mixin is the return of `test(..)` function from SassyTester. </span><span>// You can check what `$data` looks like in SassyTester documentation: </span><span>// http://kittygiraudel.com/SassyTester/#function-test </span><span><span>@mixin run($data) {</span> </span><span> <span>$tests: map-get($data, 'tests');</span> </span> <span> <span>@each $test in $tests {</span> </span><span> <span>@if map-get($test, 'fail') {</span> </span><span> <span>@error 'Failing test!</span> </span><span> <span>Expected : #{map-get($test, 'expected')}</span> </span><span> <span>Actual : #{map-get($test, 'actual')}';</span> </span> <span>} </span> <span>} </span><span>}</span>
Ideally, when Sass throws an error (either because of a built-in error or because of @error), Gulp should exit properly. Unfortunately, there is an issue about this on gulp-ruby-sass that is still not fixed so for Ruby Sass, we have to raise a Node Uncaught Fatal Exception with process.exit(1) ourselves.
Adding a pre-commit hook
There are tons of libraries to set up pre-commit hooks. I personally like pre-commit but you can basically choose the one you like as they all do more or less the same thing.
To add a pre-commit hook to our project, we need to create a pre-commit key in our package.json. This key is mapped to an array of npm scripts commands. Thus, we also need a scripts object, with a key named test, mapped to the Gulp command: gulp test.
<span>var gulp = require('gulp'); </span><span>var sass = require('gulp-sass'); </span><span>var rubySass = require('gulp-ruby-sass'); </span> <span>// Run LibSass on the tests folder </span><span>// Gulp automatically exits process in case of Sass error </span>gulp<span>.task('test:libsass', function () { </span> <span>return gulp.src('./tests/*.scss') </span> <span>.pipe(plugins.sass()); </span><span>}); </span> <span>// Run Ruby Sass on the tests folder </span><span>// Gulp manually exits process in case of Sass error </span>gulp<span>.task('test:ruby-sass', function () { </span> <span>return rubySass('./tests') </span> <span>.on('error', function (err) { </span> process<span>.exit(1); </span> <span>}); </span><span>}); </span> gulp<span>.task('test', ['test:libsass', 'test:ruby-sass']);</span>
When commiting, the pre-commit hook fires and tries to execute the test npm script. This script runs the following command: gulp test, which intimates Gulp to run the tests.
That’s it, we’re done.
Final thoughts
This example is extremely simplistic as you can see, but it does the job and it does it well. Here is what it might look like:
So what do you think? Is this something you might consider adding to your library or framework?
The above is the detailed content of Testing a Sass Library. For more information, please follow other related articles on the PHP Chinese website!

Hot AI Tools

Undress AI Tool
Undress images for free

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

There are three common ways to initiate HTTP requests in Node.js: use built-in modules, axios, and node-fetch. 1. Use the built-in http/https module without dependencies, which is suitable for basic scenarios, but requires manual processing of data stitching and error monitoring, such as using https.get() to obtain data or send POST requests through .write(); 2.axios is a third-party library based on Promise. It has concise syntax and powerful functions, supports async/await, automatic JSON conversion, interceptor, etc. It is recommended to simplify asynchronous request operations; 3.node-fetch provides a style similar to browser fetch, based on Promise and simple syntax

JavaScript data types are divided into primitive types and reference types. Primitive types include string, number, boolean, null, undefined, and symbol. The values are immutable and copies are copied when assigning values, so they do not affect each other; reference types such as objects, arrays and functions store memory addresses, and variables pointing to the same object will affect each other. Typeof and instanceof can be used to determine types, but pay attention to the historical issues of typeofnull. Understanding these two types of differences can help write more stable and reliable code.

Hello, JavaScript developers! Welcome to this week's JavaScript news! This week we will focus on: Oracle's trademark dispute with Deno, new JavaScript time objects are supported by browsers, Google Chrome updates, and some powerful developer tools. Let's get started! Oracle's trademark dispute with Deno Oracle's attempt to register a "JavaScript" trademark has caused controversy. Ryan Dahl, the creator of Node.js and Deno, has filed a petition to cancel the trademark, and he believes that JavaScript is an open standard and should not be used by Oracle

CacheAPI is a tool provided by the browser to cache network requests, which is often used in conjunction with ServiceWorker to improve website performance and offline experience. 1. It allows developers to manually store resources such as scripts, style sheets, pictures, etc.; 2. It can match cache responses according to requests; 3. It supports deleting specific caches or clearing the entire cache; 4. It can implement cache priority or network priority strategies through ServiceWorker listening to fetch events; 5. It is often used for offline support, speed up repeated access speed, preloading key resources and background update content; 6. When using it, you need to pay attention to cache version control, storage restrictions and the difference from HTTP caching mechanism.

Promise is the core mechanism for handling asynchronous operations in JavaScript. Understanding chain calls, error handling and combiners is the key to mastering their applications. 1. The chain call returns a new Promise through .then() to realize asynchronous process concatenation. Each .then() receives the previous result and can return a value or a Promise; 2. Error handling should use .catch() to catch exceptions to avoid silent failures, and can return the default value in catch to continue the process; 3. Combinators such as Promise.all() (successfully successful only after all success), Promise.race() (the first completion is returned) and Promise.allSettled() (waiting for all completions)

JavaScript array built-in methods such as .map(), .filter() and .reduce() can simplify data processing; 1) .map() is used to convert elements one to one to generate new arrays; 2) .filter() is used to filter elements by condition; 3) .reduce() is used to aggregate data as a single value; misuse should be avoided when used, resulting in side effects or performance problems.

JavaScript's event loop manages asynchronous operations by coordinating call stacks, WebAPIs, and task queues. 1. The call stack executes synchronous code, and when encountering asynchronous tasks, it is handed over to WebAPI for processing; 2. After the WebAPI completes the task in the background, it puts the callback into the corresponding queue (macro task or micro task); 3. The event loop checks whether the call stack is empty. If it is empty, the callback is taken out from the queue and pushed into the call stack for execution; 4. Micro tasks (such as Promise.then) take precedence over macro tasks (such as setTimeout); 5. Understanding the event loop helps to avoid blocking the main thread and optimize the code execution order.

Event bubbles propagate from the target element outward to the ancestor node, while event capture propagates from the outer layer inward to the target element. 1. Event bubbles: After clicking the child element, the event triggers the listener of the parent element upwards in turn. For example, after clicking the button, it outputs Childclicked first, and then Parentclicked. 2. Event capture: Set the third parameter to true, so that the listener is executed in the capture stage, such as triggering the capture listener of the parent element before clicking the button. 3. Practical uses include unified management of child element events, interception preprocessing and performance optimization. 4. The DOM event stream is divided into three stages: capture, target and bubble, and the default listener is executed in the bubble stage.
