Unit Testing 101


Continued

Oren Rubin


Applitools                            Director of R&D

Wix                                          Web Architect

IBM                                          Distributed Computing

Spring Diagnostics          Machine Learning

Cadence                                 Compiler Engineer

UI testing is hard.. so don't

JavaScript is mostly used for UI/UX

Testing the appearance is hard (bad ROI)

Don't check styles

Avoid checking for classes

Separate business logic and UI (follow MVC pattern)

Even separate the view to template and logic

Don't be scared of refactoring code

Unit Testing is on case of "reuse" of your code

Unit Isolation

Optimal

Spies

Stubs

Mocks

Unit Isolation

No app is perfect, so use

Fake ajax

Fake timers

Work asynchronously

Sinon.js


Powerful testing utility which works with ANY testing framework


Author also wrote a good book


Author Helps with Buster, an automation server

Spies

Make sure a function is called

Make sure a method is called

Make sure it was called with the appropriate params

Spy on functions


    // Function under test
    function doSomething() {
        console.log('I am doing something');
    }

    it("calls the original function", function () {
        var spy = sinon.spy(doSomething);

        spy(); // log : 'I am doing something'

        assert(spy.called);
    });
    

Spy on functions


    // Function under test
    function callSomething(callback) {
        callback();
    }

    function doSomething() {
        console.log('I am doing something');
    }

    it("spy on ", function () {
        var spy = sinon.spy(doSomething);

        callSomething(doSomething); // log : 'I am doing something'

        assert(spy.called);
    });

    

Spy on methods


    // Object under test
    var a = {
        bestMethodEva: function() {
            console.log('I am doing something');
        }
    };

    it("spy on method", function () {
        var spy = sinon.spy(a, 'bestMethodEva');

        a.bestMethodEva();

        assert(spy.called);
    });

    

Spy filters


    it("spy filters", function () {
        var spy = sinon.spy(a, 'bestMethodEva').withArgs(42);

        a.bestMethodEva();

        assert(spy.called);
    });

    

Spy assertions


    it("spy assertions", function () {
        var spy = sinon.spy(a, 'bestMethodEva');
        a.bestMethodEva();

        assert(spy.called);
        assert(spy.calledOnce);
        assertEqual(1, spy.callCount)
        assert(spy.calledBefore(sinon.spy()));
        assertFalse(spy.threw());
        assertFalse(spy.returned(undefined));

        // calledWithExactly does deep compare
        assert(spy.calledWithExactly(otherObj));
        // warning! read the API, checks if ONE invocation is similar
    });
    

Spy assertions


    it("spy riddle", function () {
        // load our component
        domElement.addEvent('click', a.bestMethodEva);

        // spy
        var spy = sinon.spy(a, 'bestMethodEva');

        // stimulate
        domElement.click();

        // assert
        assert(spy.called); // fails!! why?!
    });
    

Stubs


Test stubs are functions (spies) with pre-programmed behavior


They support the full test spy API


Stubs


    it("force a different value", function () {
        // Creates an anonymous stub function.
        var stub = sinon.stub();

        // Replace method
        var stub = sinon.stub(object, "method");

        // return specific value by:
        var stub = sinon.stub(object, "method", func);
        // or
        stub.returns(obj);
        // or
        stub.returnsArg(index); // return argument[index]
    });
    

Stubs


    it("force throwing an error", function () {
        // Causes the stub to throw an exception (Error).
        stub.throws();
        stub.throws("TypeError");
        stub.throws(obj);
    });
    

Mocks

Mocks are stubs (which also means spies) with built-in expectations
Rarely used

        var mock = sinon.mock(myAPI);
        mock.expects("method").once().throws();
        // some code
        mock.verify();
    

Jasmine

Very simple

    Great docs: http://pivotal.github.com/jasmine/


Load is by an HTML scaffold


Completely standalone


Custom Matchers: expect('moshe').toBeMoshe();


Easy to filter tests


Nested describe blocks (each has beforeEach)


    it("spy on method", function () {
        var spy = sinon.spy(a, 'bestMethodEva');
        a.bestMethodEva();

        assert(spy.called);
        assert(spy.calledOnce);
        assertEqual(1, spy.callCount)
        assert(spy.calledBefore(sinon.spy()));
        assertFalse(spy.threw());
        assertFalse(spy.returned(undefined));
    });

    

Jasmine

Jasmine Spies are full test doubles bundled together

        var yoSpy = spyOn(obj, "f");
        var yoSpy = spyOn(obj, "f").andReturn(745)
        var yoSpy = spyOn(obj, "f").andCallThrough();
        var yoSpy = spyOn(obj, "f").andCallFake(function(value) {
            return value;
        });

        obj.yo();

        expect(yoSpy).toHaveBeenCalled();
        expect(yoSpy).toHaveBeenCalledWith('moshe');
    

qUnit


load using html scaffold


Only the basic assertion


Supports fixtures

qUnit

Fixtures example

        <div id="qunit-fixtures">
            <ul/>
        </div>
        test('test add item', function() {
            var list = $('ul');
            list.append("<li>moshe</li>")
            equal(1, list.children().length, "1 child");
        });
        test('test reset', function() {
            var list = $('ul');
            equal(0, list.children().length, "no children");
        });

    

Fake timers.. time traveling

Mocking the JavaScript Clock
Used when the app uses setTimeout/setInterval for callbacks.
Used when the app uses setTimeout/setInterval for callbacks.
E.g., fast forward a transition.

Fake timers.. time traveling


        // setup
        timerCallback = jasmine.createSpy('timerCallback');
        jasmine.Clock.useMock();

        // test
        setTimeout(function() {
            timerCallback();
        }, 100);

        expect(timerCallback).not.toHaveBeenCalled();
        jasmine.Clock.tick(101);
        expect(timerCallback).toHaveBeenCalled();
    

Fake Server requests

Only Sinon has it built-in
Fake requests
Fake responses
Can go to headers level

Fake Server requests


    setUp: function () {
        this.xhr = sinon.useFakeXMLHttpRequest();
        var requests = this.requests = [];

        this.xhr.onCreate = function (xhr) {
          requests.push(xhr);
        };
    },

    tearDown: function () {
        this.xhr.restore();
    },
    

Fake Server requests


    "test should fetch comments from server" : function () {
        var callback = sinon.spy();
        myLib.getCommentsFor("/some/article", callback);
        assertEquals(1, this.requests.length);

        this.requests[0].respond(200, {
            "Content-Type": "application/json" },
             '[{ "id": 12, "comment": "Hey there" }]');

        assert(callback.calledWith([{ id: 12, comment: "Hey there" }]))
    }
    

Asynchronous


qUnit has a simple start/stop (many disadvantage)

jsTestDriver has overly smart (confusing) callback system

Jasmine has great polling (busy wait) system

Asynchronous

Jasmine

        // do something

        // wait for condition

        // assertion
    

Asynchronous

Jasmine

    runs( function() {
        // do something
    });

    waitsFor(function() {
        // return true when condition is set
    });

    runs( function() {
        // assertion
    });
    

Asynchronous

Jasmine Riddle

    console.log(1);
    runs( function() {
        console.log(2);
        // do something
    });
    console.log(3);
    waitsFor(function() {
        console.log(4);
        // return true when condition is set
    });

    console.log(5);
    runs( function() {
        console.log(6);
        // assertion
    });
    console.log(7);
    

Asynchronous

Jasmine
    var ready = false;
    var other;

    runs( function() {
        setTimeout(function() {
            ready = true;
            other = 5;
        }, 3000);
    });

    waitsFor(function() {
        return ready;
    });

    runs( function() {
        assertEquals(5, otherData);
    });

Asynchronous

jsTestDriver - hold your breath

    AsyncTest.testSomethingComplicated = function(queue) {
        var state = 0;

        queue.call('increment in 5 seconds', function(callbacks) {
            var myCallback = callbacks.add(function() {
                ++state;
            });
            window.setTimeout(myCallback, 5000);
        });

        queue.call('check other stuff', function() {
            assertEquals(1, state);
        });
    };
    

Tips

Put your tests next to the files
Have your own callLater (wrap setTimeout), and save the stacktrace
Show the skipped tested, so you never forget them
Don't be scared to to get familiar with the internals

Extras

Centralized Servers
Integrated tests

Centralized Servers

Take control of browsers (called slaves)
Usually a config file (rather than HTML scaffold)
E.g., jsTestDriver, buster

jsTestDriver

Integrated with IDEs (e.g., WebStorm, IntelliJ)
Remote Debugging
Coverage Tools!!
Demo!

Integration Tests

Usually start from front-end
Usually DOM based (Selenium, QTP, etc..)
Rarely image based (EggPlant, Sikuli)

Selenium

Navigates a browser automatically
Supports many languages (now even js)
ROI very low (for medium/big companies)

Selenium Hello World

    // also supports ChromeDriver, FirefoxDriver, and IEDriver
    WebDriver driver = new HtmlUnitDriver();

    // And now use this to visit Google
    driver.get("http://www.google.com");

    // Find the text input element by its name
    WebElement element = driver.findElement(By.name("q"));

    // Enter something to search for
    element.sendKeys("Cheese!");

    // Now submit. WebDriver will find the form for us from the element
    element.submit();

    // Check the title of the page
    System.out.println("Page title is: " + driver.getTitle());

go home





Oren Rubin

shexman@gmail.com

@shexman


advanced unit testing

By shex

advanced unit testing

  • 2,968