Writing a JS Proxy based assertion function
What are assertion functions, how to use them and how to write your own using the Javascript Proxy object
This article was inspired by Kent C. Dodds’s blog post: But really, what is a JavaScript test? In his post he explains in detail the internals of testing frameworks, he also included a short section on how assertion libraries such as node’s assert
or chai.js
work. We’re going to dive a little deeper than that and implement a chainable — high performance assertion library. We’ll cover a few different techniques, and eventually, how to optimize our library using the JS Proxy Object.
What are assertion functions
Very simply, assertion functions are functions that accept an expression or a value, and run other functions against it to see if it passes a test. They are mostly used in unit testing frameworks, but they could be useful in other scenarios as well.
How do they work?
The way most common assertion functions work is very easy to understand. If the expression passed the test, they return silently. If it didn’t they throw an error.
The testing framework then catches the error. This is how it knows whether the test failed or passed.
If no error was thrown, the testing framework assumes that test passed. That’s why empty tests are considered to be passing:
Writing our own assertion function
Assume we need to write our own assertion function. We’ll call it enforce
. Instead of being used to test functions, we will use it as a data validation API.
enforce
should test your data against rules and check if it passes your validations. We will write some validation rules and conditions, and also allow extending it with custom validation functions.
You can follow along by pasting the code snippets into your console. Let’s start.
What’s in the box?
Before starting to code, consider what needs to be done to have a fully functioning implementation of enforce.
Our assertion library needs to:
- Accept a value to be validated; Duh.
- Allow infinitely chaining validation functions so consumers can test a single value against multiple rules.
- Throw an error that alerts the testing framework the validation failed.
- Accept plugins and custom chained functions.
We also need to create our validation rules which will be used as our chained functions. You can add as many as you want, and make them as complex as you want. The important thing is that they all have the same signature, so you can reliably use the same function to chain them. These will do for now:
All the following code snippets assume this rules object is present in their scope. When testing in your console, make sure they are present as well.
Doesn’t sound so hard. What’s the problem?
Writing such an API is pretty much straightforward, but there are a few tricky parts we need to keep in mind:
Problem #1: Making the chained functions aware of the initial value
Say we have the following chain:
How would you make largerThan
aware of 55
, the initial value given to enforce?
Problem #2: Supporting infinite chains
While “endless” chain is not very practical, it is useful to allow chaining multiple rules into the same chain, and we should not limit the number of functions in the chain — so unless an error was thrown, our library needs to be able to infinitely chain functions
There are many approaches to solving these problems, we’ll cover a few of them here along with their strengths and weaknesses.
The “Naive” approach: Use `this`
What’s the problem? The obvious solution is to create an Enforce class that stores the given value on the instance and returns the rules object.
But this cannot work without making big changes to this approach:
Issue #1: Actually using the value
When using this approach, all rule functions need to be context aware, they need to be bound to this
, and instead of getting value
as an argument, they would need to access this.value
.
Issue #2: Throwing or Chaining more than one function
One of the awesome things about enforce is that it is infinitely chainable, until one of the functions throws. In order to do this, each rule needs to also be able to throw on failure, and return the original rules object on success.
Issue #3: this.value pollution
This approach has the potential to break the moment you try to run a few tests in parallel, or try to use it with an async rule. You simply don’t have any control on who’s setting this
or what its current value is.
You could dynamically, upon running your enforce generate a new this.value
property, with its own distinct name, such as this.value_0bd3
and during your run only access it — but it seems like a lot to do just to access your stored initial value.
You may also consider creating a new instance of enforce
each time you use it like this:
But it means that each time you use enforce, you rebuild the rules object. Even worse when extending enforce with custom validations — an expensive operation.
Too much of a hassle. Let’s ditch class declaration altogether, and use the “it’s christmas time” approach.
The “It’s Christmas time” approach: Let’s wrap everything
Instead of using a class constructor, maybe we should try a simpler approach: wrapping all rules with a function that passes down the initial value.
This works perfectly. Very powerful, yet very simple, and it even works:
But again, there are some serious issues with this approach:
Issue #1: How do you add plugins?
One of the things you have to remember when writing validation libraries is that you don’t know who is going to use them, and for what purposes. You must let them extend your rules with their own (or even completely replace them). Unless the consumer stores the functions in the global scope, you simply do not have an elegant way of adding the custom rules to your library. note: adding a second argument to each use is not elegant.
Issue #2: Performance!
Unlike most use cases for assertion libraries that run in testing environment; as a validation function, enforce needs to run in production. We must take performance into account. This solution is VERY labour intensive — All functions get the initial value, without polluting this
- and that's exactly the problem - all functions, no matter whether we're going to use them or not. Each time enforce runs, all the functions get iterated and wrapped in another function.
The more validation rules you have in enforce, the worse it is going to get. Multiply that number by the amount of times you run enforce
in a single form validation, and you got yourself one big performance issue.
So it’s been established, you should not wrap everything but you should also definitely not rely on this
. Then what should you do?
The “Lazy” approach: Just use Proxy
Simple. No need to overthink it. Just use Proxy.
What is a Proxy?
The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc). -MDN
Or, when put to a practical use: the Proxy object allows you to create a handler that intercepts your interaction with an object, and reroute, modify or completely prevent your interaction — and it does it all lazily — only on demand, never preemptively.
Proxies: Simple example
Lets see how proxies actually work. The following is a simple implementation of a getter. We could also make it a setter, but it is not relevant for our use case.
Putting it to actual use
Using Proxies, we can dynamically, when running enforce, wrap the currently called rule with a function that both accepts the initial value, and returns again the proxied object.
Simplifying a bit, but it looks something like this:
Quick summary of this code:
- We imported our rules object
- Created an
enforce
class - The constructor assigns
this.allRules
and extends with custom rules - the constructor returns
this.enforce
this.enforce
function creates anew Proxy
- For the Proxy target we use
this.allRules
- For the Proxy handler we use a function that:
* Checks if the called property is actually a rule
* If it is a rule, wrap the original rule with a function that has the initial value - If the validation passes, return the
Proxy
again, allowing adding more functions to the chain - If the validation fails, throw an Error
Give it a try:
And it even works great with custom rules!
Are Proxies really that much better?
So we just switched to proxies to get to the same place we were in the “it’s christmas time” approach. Does it really matter that much that we had to rewrite everything? After all, proxies are considered to be pretty heavy.
I ran a simple benchmark on the two versions of enforce
, before and after using Proxy.
Here is the code used for the benchmark:
And here are the results: by using proxies we made enforce run 5 times faster.
A note on browser compatibility
Proxy is an ES2015 feature, and as such it is not supported in older browsers. Today you should be mostly safe, as the only major use browser not supporting it is IE11, which currently holds around 2.5–3.5 market share, depending on who you ask.
If you do need to support older browsers, you should consider using GoogleChrome/proxy-polyfill. It does not provide a full polyfill, in fact, a fully functional polyfill is not even possible, but this one works just fine in our use case.
Now your code should work well on IE9+ and Safari 6+.
Real life use case for enforce
At Fiverr we’ve been working on Passable, a unit-test like data validation library. It is useful for describing data validations in the form of a spec, and testing it accordingly.
Passable comes with a built in assertion library called enforce
. You just wrote it. It works exactly the same way as your newly created function
Conclusion
In this post we learned that assertion functions are simple functions that throw an error when a certain condition was not met. We tried different approaches creating our very own assertion library, and what are the pitfalls of each. We used the Javascript Proxy object to optimize our API by dynamically wrapping the validation functions on demand rather than preemptively.
There are, of course, other methods to achieve the same goal. I mentioned the ones that we tried at Fiverr, and the progression which lead us to use the Proxy object.
Lesson learned. Be lazy.
I hope that you found this article useful.