While working with a search quality development team, I was asked to collect information from their result pages across all the supported languages. The aim was to quickly get an overview, and then manually look through them for irregularities. One option would have been to grab the pages using tools like Selenium or WebDriver. However, this would have been complex and expensive. Instead, I opted for a much simpler solution: Display each language variation in a separate IFrame within the same HTML page.
An example of how this looks, can be seen here, where the Google Search page is shown in 43 different languages. There are also some links for other Google sites, or you can type in your own link, and the query attribute for language, "hl=", will be appended at the end.
(Warning: Do not try this with YouTube, as 43 Flash windows on the same pages will crash your browser. Also, Firefox 2 is known to be slow, while Firefox 3 works fine.)
Creating the IFrames is easy using JavaScript, as can be seen in the example below. I assume that the array of languages to iterate over is retrieved by the function getLanguages(). Then a simple loop uses document.write(...) to dynamically add the IFrames. It is worth mentioning that this method seemed to be the best way of dynamically creating them; using the document.createElement(...) resulted in some complex race condition issues when adding the IFrames and their content at the same time.
var languages = getLanguages();for (var lang, i = 0; lang = languages[i]; i++) { document.write('<hr><a name="' + lang + '"/>' + '<h2>' + lang + '</h2><center>' + '<iframe src="' + url + lang + '" width="' + queryMap['width'] + '" height="' + queryMap['height'] + '"></iframe>' + </center><br/><br/>');}
The rest of the source code, can be seen in this example. Nothing else is needed to get the overview.
The example in this article shows that a very simple and inexpensive solution can be useful for exploratory testing of web pages; especially when quickly looking over the same pages in multiple languages. The small amount of code required, makes it easy to customize for any project or page, and the fact that the requests are done dynamically, gives a view which is always up to date.
Of course, this type of overview lends itself best to very simple stateless pages, which do not require complex navigation. It would for example be more difficult to get the same list of IFrames for Gmail, or other complex applications. Also, as the IFrames are loaded dynamically, no history is kept, so tracking when a potential bug was introduced in the page under test might prove more tedious.
Furthermore, it should be noted that the overview only simplifies a manual process of looking at the pages. In some situations, this might be very beneficial, and enough for the developer, while in other projects more automated tests might be designed. E.g., it could be difficult to automate tests for aesthetic issues, but easy to spot them manually, while it may prove more beneficial to automate checks for English terms in other languages.
Finally, a word on the issue of translations and language skills. The overview in this example, quickly highlights issues like incorrect line wrapping, missing strings, etc. in all variations of the page. Also, it was easy to spot strings not already translated in some of the languages, like Japanese, and in fact I reported a bug against the Search front page for this. However, for other issues, more language specific skills are necessary to spot and file bugs: E.g. should the Arabic page show Eastern or Western Arabic numerals? And have the Danes picked the English term for "Blogs", while the Norwegians and Swedish prefer a localized term? I don't know.
interface Duck {Point getLocation();void quack();void swimTo(Point p);}class ForwardingDuck implements Duck {private final Duck d;ForwardingDuck(Duck delegate) {this.d = delegate;}public Point getLocation() {return d.getLocation();}public void quack() {d.quack();}public void swimTo(Point p) {d.swimTo(p);}}
public void testDuckCrossesPoolAndQuacks() {final Duck mock = EasyMock.createStrictMock(Duck.class); mock.swimTo(FAR_SIDE);mock.quack(); // quack after the raceEasyMock.replay(mock);Duck duck = OlympicDuck.createInstance();Duck partialDuck = new ForwardingDuck(duck) { @Override public void quack() {mock.quack();}@Override public void swimTo(Point p) {mock.swimTo(p);super.swimTo(p);}// no need to @Override “Point getLocation()”}OlympicSwimmingEvent.createEventForDucks().withDistance(ONE_LENGTH).sponsoredBy(QUACKERS_CRACKERS).addParticipant(partialDuck).doRace();MatcherAssert.assertThat(duck.getLocation(), is(FAR_SIDE));EasyMock.verify(mock);
CreditCardProcessor processor = new CreditCardProcessor();
OfflineQueue queue = new OfflineQueue(); CreditCardProcessor processor = new CreditCardProcessor(); processor.setOfflineQueue(queue);
Database db = new Database(); OfflineQueue queue = new OfflineQueue(); queue.setDatabase(db); CreditCardProcessor processor = new CreditCardProcessor(); processor.setOfflineQueue(queue); processor.setDatabase(db);
Database db = new Database(); db.setUsername("username"); db.setPassword("password"); db.setUrl("jdbc:...."); OfflineQueue queue = new OfflineQueue(); queue.setDatabase(db); CreditCardProcessor processor = new CreditCardProcessor(); processor.setOfflineQueue(queue); processor.setDatabase(db);
CreditCardProcessor processor = new CreditCardProcessor(?queue?, ?db?);
Database db = new Database("username", "password", "jdbc:...."); OfflineQueue queue = new OfflineQueue(db); CreditCardProcessor processor = new CreditCardProcessor(queue, db);
class House { Door door; Window window; Roof roof; Kitchen kitchen; LivingRoom livingRoom; BedRoom bedRoom; House(Door door, Window window, Roof roof, Kitchen kitchen, LivingRoom livingRoom, BedRoom bedRoom){ this.door = Assert.notNull(door); this.window = Assert.notNull(window); this.roof = Assert.notNull(roof); this.kitchen = Assert.notNull(kitchen); this.livingRoom = Assert.notNull(livingRoom); this.bedRoom = Assert.notNull(bedRoom); } void secure() { door.lock(); window.close(); } }
testSecureHouse() { Door door = new Door(); Window window = new Window(); House house = new House(door, window, null, null, null, null); house.secure(); assertTrue(door.isLocked()); assertTrue(window.isClosed()); }
testSecureHouse() { Door door = new Door(); Window window = new Window(); House house = new House(door, window, new Roof(), new Kitchen(), new LivingRoom(), new BedRoom()); house.secure(); assertTrue(door.isLocked()); assertTrue(window.isClosed()); }
testSecureHouse() { Door door = new Door(); Window window = new Window(); House house = new House(door, window, new Roof(), new Kitchen(new Sink(new Pipes()), new Refrigerator()), new LivingRoom(new Table(), new TV(), new Sofa()), new BedRoom(new Bed(), new Closet())); house.secure(); assertTrue(door.isLocked()); assertTrue(window.isClosed()); }
MVC
MVP
public CongressionalHearingView() { testimonyWidget.addModifyListener( new ModifyListener() { public void modifyText(ModifyEvent e) { presenter.onModifyTestimony(); // presenter decides action to take }});}
public class CongressionalHearingPresenter { public void onModifyTestimony() { model.parseTestimony(view.getTestimonyText()); // manipulate model } public void setWitness(Witness w) { view.setTestimonyText(w.getTestimony()); // update view }}
public void testSetWitness() { spyView = new SpyCongressionalHearingView(); presenter = new CongressionalHearingPresenter(spyView); presenter.setWitness(new Witness(“Mark McGwire”, “I didn't do it”)); assertEquals( “I didn't do it”, spyView.getTestimonyText());}