Okay let me rephrase that. As usual, I was confused, but the error was actually pretty straightforward. At some point in my code, I was trying to call .length on undefined. This issue wasn’t resolved by using a spy and I’m not sure it could have been, so I’ll make the explanation brief. I was very confused because my code wasn’t using .length at all. As far as I could tell, it wasn’t invoking any code that would use .length either:
My file didn’t start out looking like this, but I kept stripping away complexities to make the actions of my code more obvious and easier to debug. These tests basically do nothing and the syntax they’re using has been properly imported from the necessary libraries. If that wasn’t the case, we might have seen an error coming from expect().to.equal() like we did last week. So if we take a deep breath and actually try to read our error message, we see that it’s telling us that length is being used by a function called hash, which is used by a function called attentiveFunc. Wait a second, I wrote that function…and I’m importing it!
If you’re like me, you might be asking “How could this file cause an issue? It’s imported, but the function isn’t invoked as one of our tests!” And you’d be right — the function isn’t invoked in our tests. So the first thing I learned this week is: Mocha doesn’t care. Mocha tested my function for errors even though I hadn’t invoked it, and it was finding that this function was causing problems in my environment. The function in question was written with the assumption that it would be executed after another function had set certain properties in the window object. This is always the case in production because the property-setting function is the one that invokes attentiveFunc, but this was a test environment. To fix the problem, I chained some conditionals:
const windowTestHash = hash(window.test);// working version
const windowTestHash = window.test && hash(window.test);
And the problem was solved! The original version was calling hash (a function defined in another file) on undefined, which resulted in an error. The updated version would simply assign the variable a value of undefined, but wouldn’t run the part of the code that caused the error. I had to do this a few other times in the function where these variables were referenced. Here are the results, for people who live to see passing tests:
Start here if you’re just interested in the spy
I wanted to tell that story because it better reflects something I learned at work this week, but I’m also hoping to learn about spies this weekend because I have a feeling they might be useful as I continue to develop my tests. Dear Sinon: what’s a spy?
A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. There are two types of spies: Some are anonymous functions, while others wrap methods that already exist in the system under test. (source)
Sinon’s spy is very versatile, which can often be a barrier for beginners like me. With so much to learn, I decided to focus on one very specific use case, just to see if I could get it to work. You can probably guess the first steps: install and import the package.
For reference, I’ll be working off the sample class and test file that I created in my blog last week. Feel free to give it a quick look if you want to try and code along. Once we’re caught up, our next step is to update our code and give it something new to test. A spy is especially useful in tracking abstractions, like a function being called on the way to a new result. To provide a simple example, I slightly changed our reverseString method so that it returns the reversed string rather than assigning it. Then, I created a new method, assignReversedString, which sets calls reverseString and sets its result as a class property:
In my test file, I’ve added two tests to check that the method is working the way I’d like it to:
The final test uses Sinon’s spy method, which can be invoked with 0–3 arguments. In this case, we’re providing it with an Object and a method argument and, as a result, it assigns a wrapper to the passed method that gives it additional methods and properties. Among these are the assert and calledOnce methods — we can assert that the method we’re spying on (“reverseString”) has been called once. If we run our tests, we’ll see a that beautiful success screen:
We can see that not only was the property set, but that our invocation of assignReversedString method did in fact result in the invocation of reverseString. It’s definitely useful to know that not only did we followed the expected path on the way to our results.
What else can we do?
If you want to experiment more with Sinon’s spy, their documentation provides a fantastic list of all of its properties. Another great way to learn is to actually take a peek behind the curtain by navigating into your node_modules directory. I found sinon/dir/sinon-esm, which contained a ton of properties that Sinon sets on their spy object.
This helped me realize that, while Sinon does include methods that can be called with a spy as an argument, an individual spy also has its own properties that may provide even more insight. As we can see in the screenshot, Sinon provides calledOnce, calledTwice, and calledThrice properties, but what happens beyond that?
As impractical as it may seem, we programmers know that there will always be a situation where this obscure value is useful. I won’t pretend that I know how Sinon works from a brief scan of their code, but I do see a function called incrementalCallCount that increases the value of a property called callCount. Assuming that proxy represents our spy, What if we wrote something like this?
Before we run the test, let’s look at what we’ve done. We added a new method that runs a loop to execute our method 5 times. Not a practical example? If the only thing it’s useful for is as an example to help us learn, that still means it’s pretty useful! Following our assumptions about the spy’s callCount property, we would expect it to increase by 1 each time reverseString was called. Since Sinon doesn’t provide a method that can check whether a function has been run 5 times, we can use our old friend, the Chai expect syntax, to simply check the property. On line 26, we’re asserting that the property’s value should equal 5. Let’s run our test:
Okay so not only did our new test fail, but our original counting test did as well. Eagle-eyed readers may have noticed that I moved my const declaration outside of the it statement so that it could be applied to both tests. This was actually necessary because Sinon will throw an error if we try to wrap a method that has already been wrapped by a spy. The problem here is, because of hoisting, our spy starts counting function calls the first time we run any test inside of the describe statement. My entire test suite calls reverseString 8 times, so the ultimate callCount ended up at 8. There are a few ways to get around this, but I implemented one simple fix to close things out:
We can use the resetHistory method to…reset the callCount and other properties of our spy. Let’s end with a nice screenshot of our passing tests:
Stop me if you’ve heard this before: there’s so much more to learn in the worlds of Mocha, Chai, and Sinon. We’ll keep taking things one day, one lesson, and one step at a time. There’s no such thing as wasted effort because we’re constantly learning!