I was refactoring some javascript code at work the other day. Literally, 3000 lines of logic to decide on whether to throw an alert and what to include in the message, if so. I kid you not. There's a serious problem with some of the legacy code where I work, though, admittedly, the logic was a big tricky. All but about 300 lines of that code was pure duplication (copied, more or less line for line across 10 pages). So, by the end of the day I'd reduced the 3000 lines to about 60, and added about 240 lines of Jasmine tests (still 300 lines, but now heavily tested).
Anyway, in doing this, I came up with a way to test the function which threw the potential alert messages. I thought I'd share this idea, as I rather enjoyed coding it. Either way, I wanted to document it for my own reference, and hopefully others will find it useful, too.
The gist of the problem here was that there was a single function call (triggered from clicking a 'Save' button on various pages), which susequently called a number of other functions, depending on the case being tested, each of which had the potential to add something to the alert message. I had refactored these subsequent functions to just return strings with their various contributions, and so those functions were very easy to test individually.
What was tricky, was figuring out how to test the original function that actually throws the alert, depending on the responses. How do you test an alert? I guess there are various ways, but here's what I came up with...
Imagine if you will, the 'main' function is called 'alert_me' which ultimately just returns so that the form post can be submitted. 'alert_me' calls a number of other functions, all of which use a function called 'get_val' to reference the data that they use to calculate their response (I tried to simplify this problem for this post as best I could, so we could just focus on the problem at hand. Hopefully, this makes sense.)
I set up an object to contain default values for the various possible responses of 'get_val' (for this exercise foo, bar, or baz), and a helper function to set them ('set_val'). (Note: I'm not testing 'get_val', but I need to be able to mock its output in order to get the correct response from 'alert_me'). I also use a variable to capture the alert message which I can set by overriding javascript's 'alert' function.
describe("alert_me", function() {
var get_val_rtn_val, alert_msg;
var set_val = function(field, val){ get_val_rtn_val[field] = val; };
beforeEach(function() {
get_val_rtn_val = { // reset values
'foo': 0,
'bar': 0,
'baz': 0
};
spyOn(window, 'get_val').andCallFake(function(field) {
return get_val_rtn_val[field];
});
alert_msg = '_default_';
spyOn(window, 'alert').andCallFake(function(msg) {
alert_msg = msg;
});
});
it("tests the test", function(){
expect(get_val('foo')).toEqual(0);
set_val('foo', 1);
expect(get_val('foo')).toEqual(1);
expect(alert_msg).toEqual('_default_');
alert('foobar');
expect(alert_msg).toEqual('foobar');
});
it("contains '_default_' for default case", function(){
alert_me();
expect(alert_msg).toEqual('_default_');
});
it("contains 'Wheezy' for foo=1", function(){
set_val('foo', 1);
alert_me();
expect(alert_msg).toEqual('Please note the following: Wheezy');
});
it("contains 'Dopey' for bar=1, baz=1", function(){
set_val('bar', 1);
set_val('baz', 1);
alert_me();
expect(alert_msg).toEqual('Please note the following: Dopey');
});
it("contains 'Wheezy' and 'Dopey' for foo=1, baz=1", function(){
set_val('foo', 1);
set_val('baz', 1);
alert_me();
expect(alert_msg).toEqual('Please note the following: Wheezy, Dopey');
});
});
Naturally, if 'alert_me' is working as expected, these tests should pass!
I'd love to hear feedback about this solution. It seemed pretty simple and straightforward to me. I find Jasmine to be flexible and fun to work with.
As a side note, one of the things that I really enjoyed about writing all these tests was that it made me think differently about the way I write Javascript. I believe the refactored code was a lot more elegant, largely due to the fact that it was designed to be easily tested.
Nicely done. Refactoring code and testing code is an awesome thing to do in any situation, and the fact that you can have 10% of the lines of code is awesome.
ReplyDeleteOne thing I would recommend - Overriding the window's 'alert' is good, but IMO a better way to do it would be to have your functions take as a parameter an 'alert' function that you can default to window.alert. The benefits of this are twofold - one, you get to just make a new function and pass it into the functions you're testing, but the second is that in the future, if you decide that you want to ajax a call to a server AND alert, you can just alter that one function, and not have to do more refactoring of your code (find all useages of 'alert' and replace with some other 'fauxAlert'...etc etc)
Thanks for the kind words on refactoring. I hadn't pointed out, but I was actually fixing a bug (not just refactoring for refactoring's sake). Although this application needs a lot of refactoring, we only touch code as necessary.
ReplyDeleteI might not have made this entirely clear, but in this particular case, there was only one place where alert was called (in the 'main' function which acted as an aggregator for the various other functions' message contributions).
However, the idea of wrapping the alert function, generally, is very good. I'm certain there are many other places in this application that are using javascript's alert directly.
I would love to go through this particular application and do a general refactor of all the Javascript. It's an Oracle APEX application, and lots of JS code lives in individual pages, and is repeated across pages, etc. It boggles the mind. Feels like being back in the 90s.
Anyway, thanks for the comment.