Welcome to the fourth iteration of WebCalc implementation. Previously I added multiplication and division as operations supported by my WebCalc. Especially the latter one forced me to add and change a few things in the code. By doing so I think I’ve made a mistake, that I’ll fix now.
The problem was to make Calculator stateful, it now remembers maxFractionDigits. If one user would set maxFractionDigits, all others would see that changed too. That’s actually a serious bug, I’m excused just for now and just because my application doesn’t know about users or sessions yet and have no tests verifying correct behaviour. But merely making such service a stateful one can also be perceived as an issue. No matter why I’ll fix it now.
Because I’m using TDD, I need a test verifying the presence of my bug. Here it is:
It works, it is failing because of setting something in one session affects the other. It also looks lengthy, I’ll cut it a bit to make it more readable, like so:
Still works, but I think it more says about what I want than how to do it, that’s good.
Having proven I have indeed a problem I can try to solve it. My idea is: I’ll store maxFractionDigits in HTTP session and on every call to eval function I’ll in this parameter explicitly. Now my controller looks like this:
I’ve badly broken my Calculator module API, it will cost me a bit of time to fix it. First, I’ll add this parameter to eval function but will not use it just yet.
TDD outside-in way, I’ll fix the tests first. Tests for the division are easy, because they have maxFractionDigits provided explicitly, but all other tests? I need a default value and I need to put it somewhere. Calculator class would probably be the best place to keep it, as it is doing all the calculations. As for value, I’d probably need to ask my users or product owner. Because I have none, I’d assume 2. So my tests now look like this:
Most of my division tests are now broken, as well as all REST tests. Time to fix this. It’s actually not that hard. I just delete the class field and start using the value provided to the eval function itself. The code now looks like this:
That fixes all unit tests and my newest REST test. Unfortunately, my previous REST tests are now failing because of NullPointerException. Indeed, my session doesn’t have “maxFractionDigits” parameter set by default. The fix is quite simple:
But that fixes only one of my tests, the other one with setting maxFractionDigits is still red. Indeed, it doesn’t keep track of the session. This time it’s the test that needs fixing:
Phew, back to all green, unit and REST tests. Is everything good? No, I’ve noticed a race condition in Calculator. This class is still stateful, because of formatter. On every eval function call, I’m changing its state. With bad luck, two sessions will interfere with each other. The solution is to get a new formatter and set it up every time it’s used. This time I’ll not TTD-drive it, I honestly don’t know how, or even if it is possible. I could try to create a lot of sessions and execute different calculation requests in parallel, but even that doesn’t guarantee I’d trigger the problem. Anyway, as I’m already touching this area, I’m also extracting all formatting activities to a separate method, like this:
There’s yet another thing I’ve noticed. None of my test cases is using numbers with digits after the decimal point as an input. For completeness sake, but also to check for formatting issues, I’m adding a few test cases, like so:
And, not really surprising, I have a problem. I am formatting my output, but I left input with default settings. Again, time for fixes. It looks like NumberFormat is not enough, I’d need a DecimalFormat. Luckily, I can also use DecimalFormat for formatting my output. That way input and output will be in sync. I end up with this:
This starts to get complicated. I also have some duplication, I’m creating a formatter in two different methods, but also every single time I need it, let’s get rid of it.
Better. This ParseException, that I need to handle explicitly, bothers me a bit, but I’ll need to add some proper error handling sooner or later, not now though.
One more thing to verify. The fact, that I’m limiting my output to a specific number of fraction digits doesn’t mean, that I should also be cutting my input. Example, I’m still using my default 2 fraction digits of the result and the calculation to do is to multiply 3,333 by 4,444. If I just multiply and not limit the result, I’ll get 14,811852. When I round it to 2 fraction digits I get 14,81. But if I’d first round my input to 3,33 and 4,44 respectively, then my results would be 14,7852 and 14,79 before and after rounding. So rounding or not my input does affect the final result and I don’t want the input to be rounded. But this test case will verify that. Test:
Luckily my code is correct and all tests are green.
That’s a lot of fixes or things I forgot to do earlier, even for such a simple thing like a calculator. No wonder that software we’re writing has so many bugs. It is then even more crucial to be careful, think constantly about what can go wrong and then secure yourself with tests.
OK, it doesn’t seem like I could be adding anything new to my application in this iteration, so at least I’ll upgrade a few things. Recently stable Spring Boot version 2.1.0, Java 11 were published, also Gradle released version 5.0-RC1 with Java 11 support. I’ll start with last one:
All tests still working and green. Now Java:
Still no issues, time for Spring Boot. This one also includes syncing of library versions:
Tests still running and green. I’m still annoyed by the fact, that I cannot use dependency management from Spring Boot in my Calculator module, I’m getting very weird compilation errors. This time I think I’ll try to deep dive into this because I’m probably doing something wrong. If not, I’ll file a bug.
That part was tough, I achieved not much progress, only in terms of having fewer issues in the code. Hopefully, next time will be better and I’ll actually get to the part, where multiple calculations could be combined together. See you next time!