It is JS, not X-Men. Stuck in mutation hell.

shishir kaji shrestha
5 min readJun 20, 2021
liz hiers

I was creating unit tests when I came across a problem. I was trying to deepEqual an expected object to a value received from a function. I just could not understand why they did not equate, although they were exactly the same (at least I hoped it to be equal). Then I came to know that the problem was with the ‘someObject’ that I had defined was getting mutated in different unit tests. For example.

const someObject ={
prop1:'this will ',
prop2:'mutate'
}const expectedObject ={
prop1:'something'
}describe('helloWorld test',()=>{
it('unit test 1',()=>{
SUT(someObject)
......
})
it('unit test 2',()=>{SUT(someObject)
// someObject is mutated here
......
})
it('unit test 3',()=>{const receivedValue = SUT(someObject)
......
assert.deepEqual(expectedObject,receivedValue)
})
})

In the above test, I came to realize that the ‘someObject’ was being tampered with in tests 1 and 2. By the time unit test 3 reached, the ‘someObject’ was no more the original object. Hence it would fail. Now good luck with debugging that. TBH, I understood what was happening but I did not find out where it was happening. Hence, I just created another fresh object to pass to SUT. This is when I found the consequences of impure function and mutation.

A function that causes an object in the outer scope to mutate is dangerous (Not all the time, read the conclusion to find why). It could cause unexpected behaviors/results making it hard to debug and test. In this article, I want to demonstrate a very simple example of mutation and an unexpected behavior caused by mutation.

In this tiny demo,

we will use a car manufacturing scenario. Imagine you are about to build a car. In this program, you would first ask for a frame to a ‘manufacturingLine’ object. Once you get your car frame, you can use other methods in the manufacturing line object to assemble another part of the car, for instance, a dashboard, stereo, wheels, airbags, engine, battery, etc.
After we assemble all the parts we will need to paint the car. However, we want to see what the car would look like before actually committing a color to the car we have just assembled. It would be a waste of effort if we did not like the color we used the first time on our car which is almost ready. To solve this problem, we can use a dummy car to test different colors on the wheel, number plates, and overall frame of the car. And once we finally like it we will paint our actual car to complete and get it roaring on the road.

Here is the Github repo and there are different branch with fixes :

The manufacturing line Object looks as such. All the methods in the object are impure (except createFrame)and mutate the parameter sent in the method.

the manufacturingLine object with impure methods.

Now we will try to assemble the car using one method at a time.

We will start by getting a frame first then adding seats.

creating a frame and adding seats.

We will now add a dashboard on the car and everything else.

adding dashboard and then everything else.

We will now try different colors using the addColor method.

trying different colors.

Finally, I have decided and will be coloring my car frame with blue, and number plate with green. This is the real deal, no more testing. This means that wheel rims color should be default which is vanta black. I just like it that way.

final car.

So the car should look like this and I will compare the car that I received from createMyDreamCar function with this final expected car.

final expected car.

I run the file using nodejs.

side effect.

The final complete car does not turn out to be exactly the same as the expected car, because when we tried different colors, the method tampered with our actual almostCompleteCar.

All the methods in the manufacturingLine object are impure, causing side effects, and is expensive as I have to get the wheels repainted or even get the whole thing redone, depending on the customer :). Those methods actually change the object sent to them via parameters. Hence, functions that cause mutation on the parameter are dangerous. Imagine debugging such issues on a massive codebase.

The manufacturingLine object methods can be made pure and should solve our problem. So what is a pure function? A function that takes in the x and y parameter will always return a new z and without changing the x and y. The function must not change anything outside the function’s scope. Now we will update the manufacturingLine object methods to become pure.

pure methods.

Now if we run the program, we will get the expected result.

the object did not mutate.

The methods now take parameters and don’t update them. It just uses a spread operator to create a new object.

Conclusion:

To avoid such side effects, we can use:

1. Spread operators.
2. Freeze your object if you do not wish it to be tampered with. It will fail in run time (‘in strict mode’) if any functions are trying to mutate the frozen object. Looking at the bright side we do not have side effects.

Now, finally, mutability is not always disastrous. It can be used to optimize performance in certain cases. Maciej Sikora has done brilliant work of explaining, how we can use mutability to improve performance when dealing with a huge number of data in this article. I absolutely loved it.

--

--