Testing CSS Algorithms!!!!!!! Part 2 [WiH]

Posted July 13, 2019 in Web Development


In Part 1, we went over some fundamental concepts behind unit tests, including the amazing confidence and other good things that result from writing tests for your code. We also looked a bit at why unit testing is hard with CSS…the domain-specific, declarative thing makes it difficult to get a fast test environment and its difficult to think of CSS in small pieces given how integrated it is with interface.

(Remember…this post is also WiH, Written in Haste in order to get information out of my brain and into a blog post! Perfect is the enemy of complete! Publish, publish! )

With that in mind, let’s dive right in to the setup for what I would consider a unit test for a CSS algorithm. Instead of trying to spoof the browser, it’s all about using the browser and the tools it gives us. In fact, this setup uses no special tools at all! Just some JavaScript to compare values and log feedback to the console using console.group. Automating these tests will be something else, but one thing at a time…

A few governing principles

First, a few principles about what to test (these are subject to change as I work more with this approach, but they are worth mentioning now).

Test rendered boxes and computed values.

Do not test specific property values because then you are testing which properties you used rather than testing the result of what you programmed. For example, you might use an @supports to apply a margin for IE11, but grid-gap in newer browsers. The test shouldn’t test the value of margin, but the box positioning on the screen that is a result of whatever property you used to put it there.

The main things we want to test are:

  • the rendering of boxes, agnostic of the properties used for layout
  • calc formulas and custom property assignment

That means our silver swords (is that an expression?), in the same order, are:

  • element.getBoundingClientRect() – the box.
  • parseInt( window.getComputedStyle( element ).getPropertyValue( '--property ) ) – the computed value as a number.

We need a parseInt there because the value is returned as a string by default. You will also quickly learn why it is a thing to apply units in calc expressions vs. in the property values. I definitely do not want to have to remove a rem or a px from that value over and over.

Test boxes relative to other boxes.

The test should mostly be comparing the results of getBoundingClientRect against expected values, and figuring out what bit of math you need to do to get those values. For example, asserting that parentBox.top === childBox.top means you are testing that the child is sitting exactly at the top of its parent.

In a more involved test, you may need to assert something like: someBox.top + someValueFromACustomProperty === otherBox.bottom - ( anotherValueFromACustomProperty + aThirdBox.bottom ).

Whatever that means…

Avoid media queries wherever possible in both your algorithms and your tests.

Yeah…they will make all of this way more complicated than it needs to be. I mean, we need media queries sometimes, but best to avoid them where possible, and if you are testing an algorithm that uses media queries, try to abstract the media query part out so that you can write the test apart from it.

The test methods

Programming languages often have established conventions for how to structure tests, usually in conjunction with a test suite like PHPUnit for PHP, or Mocha or Jest for JavaScript. Our tests will be written in client-side JavaScript and run in the browser, so I will use a similar style as Jest or Mocha tests with describe and it functions. There might be a way 1) to write these tests in CSS (!), or 2) to use existing JS test suites in the browser, but again, one thing at a time…

First, we can set up a function for the describe block. This will output the test results to the console in an organized manner.

function describe( groupName, tests ) {
	console.group( groupName );
	tests();
	console.groupEnd();
}

It accepts parameters of a string for the name of the test group, then a function that a callback function that should contain all of the tests.

Next, we have an it method that will be used to run each test:

function it( testDescription, test ) {
	var testResult = test();
	var color = ( false === testResult.result ) ? 'red' : 'green';

	console.log( `%c ${testDescription} ${testResult.message}`, `color:${color}` );
}

Similarly, this function accepts a string that will describe what the individual test is, and a function that is the test itself. The test function should return a testResult object that contains an entry for true or false and a message.

Finally for our setup, we have an assertEquals function that we can use to compare values and return a result object for the it method above. (I need to look more into the source of Jest and Mocha to see exactly how they handle returning results, but again, one thing at a time…).

function assertEquals( value1, value2 ) {
	let message = '';
	var expression = value1 % value2 < 1;

	if ( false === expression ) {
		message = '\n \t Fail: ' + (value1 % value2).toFixed(1);
	}

	return {
		result: expression,
		message: message
	};
}

One major thing to point out – you will notice that we are not actually asserting equality, but “within 1px equality” using the % or modulo operator to find the modulus of the operation. FizzBuzz is just the gift that keeps on giving! The modulus is the remaining amount after dividing one number by another. To be honest, I’ve been trying to write another sentence explaining this for about 10 minutes but I can’t wrap my head around it enough right this minute in order to do so…

Anyway, we can’t test for exact equality because of sub-pixel rounding in browsers. The results of getBoundingClientRect() are often very long decimals, and even if the boxes appear to be perfectly aligned on the screen, they may have slightly different values. So, this modulus approach checks that the values are within 1px of each other.

Next up…the test!

Okay, I told you this was written in haste…maybe you wouldn’t have known and maybe people do this all the time, but I feel obligated to preface many posts I write with this information, apparently.

Now it’s time to head home from the lovely Pittsburgh establishment where I am now called Ineffable Ca Phe. I had a lemongrass chicken bahn mi sandwich. Great vibes here, I highly recommend it. Another weekend I had a really good breakfast bagel sandwich, too. There are probably not too many places that sell bagels and bahn mi! Time to go home and pack for Barcelona! Bye!

Comments

What do you think? Do you have any questions, thoughts, or related links to share? Did I make a mistake in my post?

Submit a Comment