User Interfaces and Unittesting with the QUnit framework and jQuery

Just a while ago I checked out QUnit. I fiddled around with it, wrote some simple tests and I thought, this could be very useful. When writing code you always add some new features into existing functionality and that’s where the errors and regressions appear, especially when it gets complex.

As we know, unit testing is great for frameworks with a lot of different functions depending on each other (that’s one reason the jQuery-team developed or uses QUnit for testing). But how about testing user interfaces or simulating user interaction on websites? On websites we sometimes use small bits of code responding to the interaction or the input of a user, for example hiding or toggling elements or dynamicaly change content. When we modify parts of the code we don’t want to click every button to test if everything is responding normal.

There is one way to test parts of your interface without modifing your existing code (I don’t know if this is the best way but it’s one way to run tests in a simple way).

1. Setup your test environment

When you download QUnit, it comes with a basic test environment we can use. It looks something like that:

[...]
<link rel="stylesheet" href="../qunit/qunit.css" type="text/css" media="screen">
<script type="text/javascript" src="../qunit/qunit.js"></script>
[...]
<h1 id="qunit-header">QUnit Test Suite</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests">
</ol>
<div id="qunit-fixture">test markup</div>
[...]

2. Modify the test envoironment for our use

First, I added jQuery to our test envoironment, mainly to use the DOM ready provided by jQuery. Second, I added an iframe where the interface/website we want to test is placed:

[...]
<link rel="stylesheet" href="../qunit/qunit.css" type="text/css" media="screen"> 
<!-- add jQuery for the testing environment --> 
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="../qunit/qunit.js"></script> 
[...]
<h1 id="qunit-header">QUnit Test Suite</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests">
</ol>
<div id="qunit-fixture">test markup</div>
<iframe src="interface-we-want-to-test.html" height="240" width="320"></iframe>
[...]

3. Get the jQuery-Object from your original code

After the DOM ready we have two different jQuery objects in the browser. The one from the test environment and our original object in the iframe. To avoid any trouble we „cleanup“ the standard $ object in our test environment and get a new jQuery object in noConflict mode named „$q“ (for QUnit);

[...]
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="../qunit/qunit.js"></script>
<script type="text/javascript" charset="utf-8">
var $q = jQuery.noConflict(true),
$ = null,
jQuery = null;
</script>
[...]

4. Get the „original“ jQuery object

The next step is to get the jQuery object from the iframe to trigger events like clicks, hovers or submits in your interface. We can do this after the DOM ready of the test environment:

[...]
<div id="qunit-fixture">test markup</div>
<iframe src="interface-we-want-to-test.html" height="240" width="320"></iframe>
</body>
<script>
// on DOM ready
$q(function() {   // fire event when iframe is ready
    $q('#testframe').load(function() {
        // Get the jQuery Object from the original code
        $ = window.frames[0].jQuery;
    });
});
</script>
[...]

5. Let’s test

After the setup we can run a simple test. This is very basic…just an example.

[...]
// on DOM ready
$q(function() {   
    // fire event when iframe is ready   
    $q('#testframe').load(function() {      
        // Get the jQuery Object from the original code      
        $ = window.frames[0].jQuery;      
        test('check if dialog closes', 3, function() {         
            // On start this is visible         
            ok($('#dialog').is(':visible'));

            // Simulate a click         
            $('#close-dialog').click();         

            // Now #dialog should be hidden...         
            ok(!$('#dialog').is(':visible'));         

            // Click again         
            $('#close-dialog').click();         

            // #dialog should still be hidden...         
            ok(!$('#dialog').is(':visible'));      
        });   
    }); 
});
[...]

Though, this example isn’t very complex or logical but if we change (for some unknown reasons) our original code from:

$('#close-dialog').click(function() { $('#dialog').hide(); });

to

$('#close-dialog').click(function() { $('#dialog').toggle(); });

our test will fail, because after the second click the dialog would be visible again.

Some other test checking the error validation of a ajax form could look like this:

test('check if form throws error on email validation', function() {   
    // Write some invalid values to the email field   
    $('form input#email').val('somewrong.email.com');   

    // submit the form   
    $('form').submit();   

    // check if the input field gets an error   
    ok($('input#email').hasClass('error'));
}); 

Conclusion

I don’t know how complex the tests can get and I’m shure you can’t test everything but this is one aproach to test interface functions and avoid regressions without clicking through your interface and checking every response. Of course, this is just for testing the code and the response of the interface and not the usability. (This stuff was just tested in Google Chrome, no warranty for other browsers)

Download example or check the the GitHub Repository

Note: Make sure you run this on a server (or your local server) otherwise you will have problems accessing the iframe via JavaScript.

UPDATE:

  • 2012/02/25: Added a download link with a „working demo“ after Jesse Breuer made the proposal in the comments.

Smashing Magazine Web Design Challenge

So, this is the first time I write about work I’ve done on my blog and I think the Smashing Magazine Web Design Challange is a great chance to do this and look at my own work with some critical thinking. The project I’m writing about is a website/blog I made for a friend some month ago. We know each other since we were 12…13 years old, and he always had the plan to become an actor. So after school he studied acting and he is about to finish his studies. So this website is kind of a portfolio to promote him and the projects he had done during the last years and I am supporting him with my webdesign and development skills.

Typography

The best thing was, I had a lot of good atmospheric pictures of him I could use. If you are a scout or into casting you search for a specific type of person or character and a picture (or a video) is the best way to transport that to this audience. The first thing you see when entering the page is this big box with pictures of the person, so you get this impression right away.

Color

I decided not to use much colors in the layout. The pictures are impressive enough, you don’t need to much colorfulness around them. The orange I used for the links needed to be striking to catch the eye and be warm in contrast to the very calm base of the layout.

Some self criticism: When I criticize layouts of others, mostly I my suggestion is, „can we make it more colorful?“.

Conclusion

I’m somehow out of the „designing business“ for a while because I work as a front end developer since I graduated. But sometimes I am the one giving feedback to the designers and I am the one asking the questions. This project was like testing if I still can design a website and not just developing the code underneath the hood.

And now, you’re pleased to comment and criticize!

  • Kommentare deaktiviert

Sticky scrolling shoppingcart with jQuery

Sticky scrolling shoppingcart with jQuery

This is a little jQuery snippet for a sticky scrolling shoppingcart like the one Apple uses at the Apple Onlinestore: http://store.apple.com/de/configure/MB950D/A?mco=MTM3NTgzMTk

 

Demo: http://michelgotta.de/demo-sticky-scrolling-shopping-cart/


jQuery:

$(document).ready(function() {  
    // check where the shoppingcart-div is  
    var offset = $('#shopping-cart').offset();  
    $(window).scroll(function () {    
        var scrollTop = $(window).scrollTop(); 
        // check the visible top of the browser     
        if (offset.top<scrollTop) {
            $('#shopping-cart').addClass('fixed'); 
        } else {
            $('#shopping-cart').removeClass('fixed');   
        }
    }); 
}); 


CSS:

 .fixed {    
    position: fixed;     
    top: 20px;    
    margin-left: 720px;    
    background-color: #0f0 ! important; 
} 

How to Effectively use the Repository and Query Object of Extbase?

Extbase[1] supports very well the idea of Domain-Driven Design[2]. It supplies a Repository class to comfortably fetch your objects from. Some handy methods are already implemented. Let’s have a look at some example code.

$blog = new Tx_BlogExample_Domain_Model_Blog('TYPO3 Development');
$blog->setDescription('A blog for professional TYPO3 developers.');

$administrator = new Tx_BlogExample_Domain_Model_Administrator();
$administrator->setName('Jochen Rau');
$administrator->setEmail('jochen.rau@example.com');
$blog->setAdministrator($administrator);

$post = new Tx_BlogExample_Domain_Model_Post('Effectively use the Query Object of Extbase');
$post->setContent('Extbase is a framework to develop ... hey, we\'re getting recursive now!');

$comment = new Tx_BlogExample_Domain_Model_Comment;
$comment->setDate(new DateTime);
$comment->setAuthor('Peter Pan');
$comment->setEmail('peter.pan@example.com');
$comment->setContent('Do you need some pills?.');
$post->addComment($comment);

$blog->addPost($post);

$blogRepository = t3lib_div::makeInstance('Tx_BlogExample_Domain_Repository_BlogRepository')
$blogRepository->add($blog);

In a first step, we create a new blog with an administrator, and a post with a comment. We instanciate the BlogRepository in line 21 and add the blog to it. That’s all we have to do. The blog is now persisted and can be fetched later from the repository again. We don’t have to say save($blog) at any time. The repository “takes care” of the blog.
Keep in mind that you have to instanciate the repository with t3lib_div::makeInstance() as it is a Singleton[3]. If you instanciate it with new the Repository will be always an empty one if Extbase searches for objects to be persisted.

The BlogRepository class is as simple as this:

class Tx_BlogExample_Domain_Repository_BlogRepository 
    extends Tx_Extbase_Persistence_Repository {
}

Most of the methods needed are already implemented in the Tx_Extbase_Persistence_Repository. These are:

$repository->add($object)
$repository->remove($object)
$repository->replace($existingObject, $newObject)
$repository->update($modifiedObject)
$repository->findAll()
$repository->countAll() // Since Extbase 1.1
$repository->removeAll()
$repository->createQuery()
$repository->countByProperty($value) // Since Extbase 1.1
$repository->findByProperty($value)
$repository->findOneByProperty($value)

The methods add() and remove() are self-explanatory. replace() takes an untracked object and puts it at the “place” of an existing one where update() just takes an existing object and updates it with the property values of the modified object.

The findByProperty() and findOneByProperty() methods are magic methods where you can replace “Property” with the name of a property of the Blog object. For example we can write $blogRepository->findByTitle('My Blog') to fetch all Blogs with the given title. This also works for objects like in $blogRepository->findByAdministrator($administrator) but not yet for properties holding aggregates. Thus, $blogRepository->findByPost($post) won’t work by now. findOneByProperty() is pretty similar to findByProperty() but returns the only first Object found.

The countAll() and countByProperty() methods are similar to the according find methods. But the don’t build objects. They only count resulting objects.

All these methods keep the underlying storage solution (mostly a relational database) transparent to the user[4]. But what if we have special constraints that are not covered with the methods above? Let’s say we want to find the five most recent posts of a given blog. Then we can use a Query object. Let’s ask the PostRepository for those posts:

class Tx_BlogExample_Domain_Repository_PostRepository
    extends Tx_Extbase_Persistence_Repository {

    public function findRecentByBlog(Tx_BlogExample_Domain_Model_Blog $blog, $limit = 5) {
        $query = $this->createQuery();
        $query->matching($query->equals('blog', $blog));
        $query->setOrderings(array('date' => Tx_Extbase_Persistence_QueryInterface::ORDER_DESCENDING));
        $query->setLimit((integer)$limit);
        $posts = $query->execute();
        return $posts;
    }
}

The method createQuery() returns an appropriate Query object suitable for Post objects. In Line 6 we define the constraint the posts should match: $query->matching([constraint]). The constraint is returned by $query->equals('blog', $blog). There are some more methods returning a constraint:

We can define the orderings with $query->setOrderings([orderings]) where [orderings] is expected to be an array of property-order pairs. There are two constants defining the order:

  • Order ascending: Tx_Extbase_Persistence_QueryInterface::ORDER_ASCENDING
  • Order descending: Tx_Extbase_Persistence_QueryInterface::ORDER_DESCENDING

We limit the result set to a maximum of five posts with $query->setLimit((integer)$limit). And finally invoke $query->execute() which returns us the already build posts with all their comments inside (if we are not “lazy” 😉 ). There is also a method $query->count() which does not fetch the resulting posts but counts them. Note that the Query object enables chaining. Thus, the following code does the same as the given above:

public function findRecentByBlog(Tx_BlogExample_Domain_Model_Blog $blog, $limit = 5) {
    $query = $this->createQuery();
    return $query->matching($query->equals('blog', $blog))
        ->setOrderings(array('date' => Tx_Extbase_Persistence_QueryInterface::ORDER_DESCENDING))
        ->setLimit((integer)$limit)
        ->execute();
}

That’s it for today. I hope you got a deeper insight and I am looking forward to your comments. Maybe you want to join the discussion on the related mailing list. I have taken most of the code from the extension BlogExample which is available in the Extension Repository (TER).

Footnotes

  1. Extbase is a framework to develop Extensions for TYPO3.
  2. For a brief introduction in DDD visit www.typo3-media.com
  3. A Singleton is a design pattern to ensure the uniqueness of an object throughout a given scope.
  4. “User” means in our case the Controller object in which we fetch the blogs and hand them to the View. More on the Model-View-Controller Pattern in a later post.

Info: This post was originally posted on http://blog.typoplanet.de (2010/01/27) by Jochen Rau but the blog disapeared from the web

Startuplektüre – Leseempfehlungen der intelligentesten Gründer

Die Seite www.startuplektuere.de sammelt Buch- und Leseempfehlungen, mit den deutschen Titeln, der schlausten und bekanntesten Köpfe des Start-Up-Szene und alten Hasen der IT Branche wie Bill Gates und Steve Jobs.

Denn hinter all den innovativen Ideen, Apps und Produkten der Start-Ups im Silicon Valley stecken kluge Köpfe und ihre bahnbrechende Ideen basieren auf Wissen. Dieses Wissen kreativ zu kombinieren und uns Lösungen für große und kleine Probleme zu präsentieren ist das Erfolgsrezept von jedem guten Geschäftsmodell.

Die Seite mit den Buchtipps lädt zum stöbern ein und man bekommt Lust sich auf die Suche der nächsten großen Idee zu machen. Fehlen nur ein paar Gesichter aus der deutschen Gründerszene.

Fachhochschule Darmstadt Studienführer 1973

Studienfuehrer-73
  • Kommentare deaktiviert

Fachhochschule Darmstadt Studienführer 1972

Fachhochschule Darmstadt Studienführer 1972

  • Kommentare deaktiviert

Blam!

  • Kommentare deaktiviert

Random.

 

Random.

This overhyped style is way too random to last.

  • Kommentare deaktiviert