Tuesday, 17 June 2008

Functional testing JavaScript with QUnit - initial steps

With the JavaScript I have written - I describe the tests which exercise the functions in the .js file and manipulate the variables in the .js file as Unit tests. Then when I test functions which interact with the html page that I import the .js file into - I class that as Functional testing. In this exciting episode of "Adventures in JavaScript" I describe my initial attempts at Functional testing with QUnit.

For a while I thought how could I make test the functions which interact with the html page as Unit tests? Perhaps I should learn how to use JSMock to mock out the html elements that I want to use in live? But I decided to take the easy route and in the test html file, just 'mock up' a crude version of the form I want to use when I go live. By my definition, my Unit test suite started to include some functional tests.
So how does that help me? Well, suddenly I have a suite of 'unit' tests which I can use to test the browser interaction for cross browser compatibility. And did they find cross browser bugs - yes indeedy, but if I tell you now, then I get ahead of myself so... one step at a time.
Create a test to drive the creation of the 'mock' GUI.

module("functional testing");

test("html input output elements exist",function(){
 expect(2);

 ok(document.getElementById("inputtext").innerHTML!=undefined, "input text field exists");
 ok(document.getElementById("eprimeoutput").innerHTML!=undefined,"output text field exists");

});



This test should only pass when I have some elements with IDs "inputtext" and "eprimeoutput". So I create a small form at the bottom of my test page to represent my 'production' environment.


 


Now I did a little more than the test suggested I should, but... hey ho.

My test passed, so now I can start writing code to output the variables stored in my .js file.

But I can't do that without a test, so...


test("display output text in html element",function(){
 expect(1);
        
        outputText = "<p>boo</p>"
        
        writeTheOutputTextToTheScreen();
        
 equals(document.getElementById("eprimeoutput").innerHTML,"<p>boo</p>");
});



Then I can write the code for this:


 function writeTheOutputTextToTheScreen(){
  document.getElementById("eprimeoutput").innerHTML = outputText;
 }



And so on, until I have a nice suite of tests and covered code, all of it running in FireFox - so off I duly go to try it in Internet Explorer... wha! That test failed. OK, so now my JavaScript knowledge has let me down and I have written code which does not run on both browsers. The JavaScript Gurus among you can probably spot the error here.


IE decides to uppercase all the html I inject so that

  • <p>boo</p>
becomes

  • <P>boo</P>

Which obviously helps my functional testing no end! And worse - if I change the <p> to <P> to make the test pass in IE, then FireFox lowercases everything so the tests fail in FireFox. Sheesh!

What to do?

OK...hack around in the DOM the old fashioned way, rather than compare the innerHTML...


test("display output text in html element",function(){
 expect(2);
        
        outputText = "<p>boo</p>"
        
        writeTheOutputTextToTheScreen();
        
 equals(document.getElementById("eprimeoutput").getElementsByTagName("p").length,1,"contains 1 para");
        equals(document.getElementById("eprimeoutput").getElementsByTagName("p")[0].innerHTML,"boo","contains boo");
});



Woo hoo - a cross browser functional test - however minor.

But lesson learned - I have to use the DOM functions rather than relying on innerHTML.  (don't worry - I shall get around to using JQuery eventually - I have to learn the basics the hard way first.)

This does make some of my tests rather large though...


test("check and output eprime text",function(){
 expect(21);

 document.getElementById("inputtext").value="So Isn't this being some bad text or rather Bob's WASn't he's being good";
 
 checkThisTextForEprime(document.getElementById("inputtext").value);
 writeTheOutputTextToTheScreen();
 
 equals(outputText, "<p>" + "So " + inEPrimeOutputFormat("Isn't") + 
                                   " this " + inEPrimeOutputFormat("being") +  
                                   " some bad text or rather " + 
                                   inPossibleEPrimeOutputFormat("Bob's") + " " +
                                   inEPrimeOutputFormat("WASn't") +  " " + 
                                   inPossibleEPrimeOutputFormat("he's") + " " + 
                                   inEPrimeOutputFormat("being") + " good</p>");

        para = document.getElementById("eprimeoutput").getElementsByTagName("p");
 equals(para[0].childNodes.length,13,"contains 1 elements");
        equals(para[0].childNodes[0].nodeValue,"So ");
        equals(para[0].childNodes[1].className,"ep_violation");
        equals(para[0].childNodes[1].innerHTML,"Isn't");
        equals(para[0].childNodes[2].nodeValue," this ");
        equals(para[0].childNodes[3].className,"ep_violation");
        equals(para[0].childNodes[3].innerHTML,"being");
        equals(para[0].childNodes[4].nodeValue," some bad text or rather ");
        equals(para[0].childNodes[5].className,"ep_warning");
        equals(para[0].childNodes[5].innerHTML,"Bob's");
        equals(para[0].childNodes[6].nodeValue," ");
        equals(para[0].childNodes[7].className,"ep_violation");
        equals(para[0].childNodes[7].innerHTML,"WASn't");
 equals(para[0].childNodes[8].nodeValue," ");
        equals(para[0].childNodes[9].className,"ep_warning");
        equals(para[0].childNodes[9].innerHTML,"he's");
        equals(para[0].childNodes[10].nodeValue," ");
        equals(para[0].childNodes[11].className,"ep_violation");
        equals(para[0].childNodes[11].innerHTML,"being");
        equals(para[0].childNodes[12].nodeValue," good");
    }); 



But that will teach me for writing non-TDD tests. You can tell I did not create this using a TDD cycle because some lines of the test did not lead to the generation of lines of application code - I wrote a "well I'll just check a few conditions in the one test just to be sure" test.

Writing functional tests like these (I wrote a few more like this) allowed me to discover other differences between IE and FireFox that caused me issues. e.g. To display some stats I created another FORM with some labels to display the information:
 



I do not know exactly, what possessed me to write the GUI like that, but I did. And I discovered that my code to write to the innerHTML of the TEXT worked on FireFox but not in IE, so because of my failing functional tests I rewrote this to the far simpler:
 
  • Word Count:
  • Discouraged Words:
  • Possible Violations:



So "Hooray" for simple functional tests.

I do believe that I could simplify much of the code I have written above by using the JQuery library, and that I could also use JQuery to help me test my event code, and probably create the GUI I want from within the code itself. So I shall probably try that next time in the ever more exciting "Adventures in JavaScript TDD with QUnit".


Note: TDD does not guarantee a bug free product. I know that I have bugs in the code I have written because I find them with additional exploratory testing. But I make sure that for each bug I find - I create a failing test so that when I fix it, I know I did enough... at least until the next bug.




Also Note: I have not figured out the correct set of Wordpress plugins to use to display source code so the &gt; and &lt; in the above code looks like > and < in my code.

1 comment:

  1. [...] Clicking the buttons in QUnit functional testing with JQuery (tags: jquery javascript test qunit) Functional testing JavaScript with QUnit - initial steps (tags: jquery javascript test qunit) Test Driven JavaScript using QUnit (tags: jquery javascript [...]

    ReplyDelete