Understanding the Html5 for mobile – Part 5

In this part, I am going to write the tests for view layer.

Here is the plan:

Part1 App written completely in JSF and totally server-side
Source code

Article

Article (Turkish)
Part2 Converting to a Javascript client, using Servlets for web services
Source code

Article

Article (Turkish)
Part3 Optimizing the Javascript code for unit tests, using REST (used Apache CXF) for web
services

Source code

Article
Part4 Writing mock tests and running them with JsTestDriver
Source code

Article
Part5 Writing view tests
Source code


Article
Part6 Using local storage for caching the wordbook on modern browsers
Source code
Part7 Using WebSqlDatabase for caching the wordbook on modern browsers
Source code
Part8 Using offline cache for resources, making the application support running offline
Source code
Part9 Embedding the application in a Android WebView
Part10 Bundling the application with PhoneGap
Part11 Preparing for production and distribution
Part12 Integrating the JS unit tests with continuous build systems
Part13 Measuring Javascript code coverage with JsTestDriver

In the previous parts, I separated the UI code from application logic and I wrote tests for the application logic layer. Now it is time to write the view tests.

In this part, basically I am going to test my JQuery calls. Instead of mocking with Jack, I am going to check if the expected changes are done in the document. For this, I use the HtmlDoc feauture of JSTestDriver. This feature allows us to make DOM changes for real on Html, which can be created in a very easy way.

AppViewTest.prototype.setUp = function() {
	appView = new artikelApp.AppView();
	
	/*:DOC += <div id="answers"></div>*/
	/*:DOC += <div id="nextWordContainer"></div>*/

	/*:DOC += <div id="article">old article</div>*/
	/*:DOC += <div id="translation">old translation</div>*/
	/*:DOC += <div id="word">old word</div>*/

	/*:DOC += <button id="answerDer"></button>*/
	/*:DOC += <button id="answerDie"></button>*/
	/*:DOC += <button id="answerDas"></button>*/
	/*:DOC += <button id="nextWord"></button>*/
};

In the setup function of the testcase, I created some Html elements which are the mocks of the real Html page.

Then, I tested the functions in a very easy way:

AppViewTest.prototype.testAppViewShouldShowResult = function() {

    appView.showResult();

    assertFalse('Answers must be hidden', $('div#answers').is(":visible"));
    assertTrue ('Article must be shown',  $('div#article').is(":visible"));
    assertTrue ('Answers must be shown', $('div#translation').is(":visible"));
    assertTrue ('Answers must be shown', $('div#nextWordContainer').is(":visible"));
};

for this function:

this.showResult = function() {
	$('#answers').hide();
	$('#article').show();
	$('#translation').show();
	$('#nextWordContainer').show();
};

Understanding the Html5 for mobile – Part 4

In this part, I am going to write some mock tests and run them with JsTestDriver.

Here is the plan:

Part1 App written completely in JSF and totally server-side
Source code

Article

Article (Turkish)
Part2 Converting to a Javascript client, using Servlets for web services
Source code

Article

Article (Turkish)
Part3 Optimizing the Javascript code for unit tests, using REST (used Apache CXF) for web
services

Source code

Article
Part4 Writing mock tests and running them with JsTestDriver

Source code



Article

Part5 Writing view tests
Source code

Article
Part6 Using local storage for caching the wordbook on modern browsers
Source code
Part7 Using WebSqlDatabase for caching the wordbook on modern browsers
Source code
Part8 Using offline cache for resources, making the application support running offline
Source code
Part9 Embedding the application in a Android WebView
Part10 Bundling the application with PhoneGap
Part11 Preparing for production and distribution
Part12 Integrating the JS unit tests with continuous build systems
Part13 Measuring Javascript code coverage with JsTestDriver

The Javascript code from the previous part was optimized for testing and now I am gonna write some tests.

First I want to mention JsTestDriver. It is a Javascript test runner which has a unusual approach. You can read more from their website, but it basically:

  • Creates a testing server which sends testing code to browsers and receives the test results sent from browsers
  • Uses slave browsers to run tests on

So, we’re really running our Javascript tests on real browsers. Since you can bind multiple browsers, you are able to test your JS code in multiple browsers and multiple browser versions.

This approach allows us to test the Javascript in a continuous integration manner with having slave browsers bound to the testing server and sending the test codes from the testing server. What we could also use is Rhino. With Rhino engine, we would be able to test our Javascript in a headless environment (to simplify: without real browsers), but then our application would be only tested for Mozilla (not sure how up-to-date Rhino is). So this is an advantage of JsTestDriver, to be able to have your code tested on multiple “real” browsers; on the other hand it is a disadvantage because it is hard to integrate JsTestDriver into continuous build systems. I am gonna explain how to do the integration in Part 12.

In this project, I used the Maven plugin for JsTestDriver which opens a server, binds multiple browsers to it, makes the server send the test codes to browsers and make the server receive the results. If some tests are failed, then so does the Maven build.

I will explain how to integrate it later on this article.

For mocking, I used Jack. With Jack we can create mock objects and we can verify the behavior.  Best is explaining with some code.

First let’s remember some bits of the AppController:

artikelApp.AppController = function(appView, wordManager) {

    /** @private */
    var init = function() {
        ...
    };

    /** @public */
    this.start = function(){
        appView.registerPageCreateHandler(init);
    };

};

So, here let’s say we want to verify the behavior of the “start” method which is called in the Html page (not shown here).

What “start” method does is pretty simple, it calls “registerPageCreateHandler” method of appView with method “init” passed as a parameter.

And here is the test for this part:

var jc;    //the Jack Context

var appController;

var appView;

AppControllerTest = TestCase("AppControllerTest");

AppControllerTest.prototype.setUp = function() {
   jc = createJackContext();

   appView = jc.create('appView', [
   'registerPageCreateHandler',
   ...
]);
...

appController = new artikelApp.AppController(appView, wordManager);
   ...
};

AppControllerTest.prototype.testStartShouldRegisterPageCreateHandlerOnView = function() {
   jc(function() {
      jc.expect("appView.registerPageCreateHandler")
         .once()
         .withArguments(appController.init);
      appController.start();
   });
};

  • First, I defined a Jack Context (“jc”) global variable, which is initialized in the “setUp” method of the testcase. JsTestDriver calls “setUp” method before running each test method, thus we have a new Jack context in each test method.
  • In “setUp”, which is called before every test method, I created a brand new “appView” object and passed that to a brand new “appController”. As you can see, “appView” is a mock object and I defined the mock methods by their name.
  • On test method (“testStartShouldRegisterPageCreateHandlerOnView”), in our Jack context (which is recreated on each test method :) ) I defined some expectations which are pretty self explanatory. Then I called the method I want to test. After this call, Jack context is ended and Jack verified the expectations.

One problem I didn’t mention is, “init” method of “appController” is private (not global), thus Jack or no other code in this test can’t access it. So, I applied a workaround, which I don’t like (any improvement is really appreciated):

artikelApp.AppController = function(appView, wordManager) {
   var instance = this;
   ....
   this._makeAllPublicForTests = function(){
      instance.init = init;
   ...
   };
};

Now, when “makeAllPublicForTests” is called, the method “init” is accessible from outside, and our test can access it.

AppControllerTest.prototype.setUp = function() {
   ...
   appController._makeAllPublicForTests();
};

It is a little bit ugly, but OK.

Here is a more complex code and its test:

artikelApp.AppController = function(appView, wordManager) {

var instance = this;

var currentTranslation = null;
var currentArticle = null;
var score = 0;

...

/** @private */
var answerDas = function() {
   answer('das');
};

/** @private */
var answer = function(article) {
   var correctAnswer = (article === currentArticle);

   if (correctAnswer)
      score++;
   else
      score--;

   appView.setArticleColor(correctAnswer);

   appView.setScore(score);

   appView.showResult();

};

/**
* Should only be used for tests
* @public
*/
this._makeAllPublicForTests = function(){
   ...
   instance.answerDas = answerDas;
   instance.answer = answer;
};

/** ... */
this._setScore = function(newScore){
   score = newScore;
};

/** ... */
this._setCurrentArticle = function(newArticle){
   currentArticle = newArticle;
};

/** ... */
this._setCurrentTranslation = function(newTranslation){
   currentTranslation = newTranslation;
};

/** ... */
this._getScore = function(){
   return score;
};

...
};

The behaviors we need to verify are:

  • Increasing the score if the answer is correct
  • Setting the article color based on the result if the answer is correct
  • Setting new score on the view
  • Showing result on view

...
AppControllerTest.prototype.setUp = function() {
   jc = createJackContext();

   appView = jc.create('appView', [
      ...
      'registerAnswerDasButtonHandler',
      'setWord',
      'setTranslation',
      'setArticle',
      'setScore',
      ...
      'showResult',
      'setArticleColor',
      ...  ]);
   ...

   appController = new artikelApp.AppController(appView, wordManager);
   appController._makeAllPublicForTests();
};

...

AppControllerTest.prototype.testWrongAnswerShouldBeHandled = function() {
   jc(function() {

      appController._setScore(10);
      appController._setCurrentArticle('der');
      appController._setCurrentTranslation('initial translation');

      jc.expect("appView.setArticleColor").once().withArguments(false);
      jc.expect("appView.setScore").once().withArguments(9);
      jc.expect("appView.showResult").once();

      appController.answer('das');

      assertEquals('Score must be decreased', 9, appController._getScore());
   });
};

Ok, now we have tests written, but how to run them? Now JsTestDriver part:

  • We need to define a “jsTestDriver.conf” file which holds the configuration for JsTestDriver server.


server: http://localhost:10000

load:
- src/main/webapp/resources/js/thirdparty/jquery-1.5.js

- src/main/webapp/resources/js/mine/controller/*.js
- src/main/webapp/resources/js/mine/manager/*.js

- src/test/webapp/resources/js/thirdparty/*.js

- src/test/webapp/resources/js/mine/controller/*.js
- src/test/webapp/resources/js/mine/manager/*.js

Jack is included with the definition <code>  – src/test/webapp/resources/js/thirdparty/*.js<code>.

  • Define the maven plugin configuration:

<plugin>
   <groupId>com.google.jstestdriver</groupId>
   <artifactId>maven-jstestdriver-plugin</artifactId>
   <version>${jsTestDriver.mavenPlugin.version}</version>
   <executions>
      <execution>
         <id>run-tests</id>
         <phase>test</phase>
         <goals>
            <goal>test</goal>
         </goals>
         <configuration>
            <browser>chromium-browser, /usr/bin/firefox</browser>
            <port>10000</port>
            <basePath>${project.basedir}</basePath>
         </configuration>
      </execution>
   </executions>
</plugin>

Important things are

  • I defined the “basePath” as project base directory, thus I need to specify the resources relative to that folder (shown above in jsTestDriver.conf)
  • I gave the same port number in the server’s definition : 10000

Now when I run “mvn test”, JsTestDriver Maven plugin will open the browsers defined in the configuration automatically, binds them to server and closes them (or the opened tabs) when the testing is done.

Here is a sample output:

-------------------------------------------
J S  T E S T  D R I V E R
-------------------------------------------

................
Total 16 tests (Passed: 16; Fails: 0; Errors: 0) (29.00 ms)
Firefox 5.0 Linux: Run 8 tests (Passed: 8; Fails: 0; Errors 0) (28.00 ms)
Chrome 12.0.742.112 Linux: Run 8 tests (Passed: 8; Fails: 0; Errors 0) (29.00 ms)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

If you take a look at the code here, you will see that I use a Maven profile for Javascript unit tests. I did it because I wanted to trigger it only when I want to. It is really annoying to see browsers opening and closing while doing a install. I put the JsTestDriver execution into the profile called “jsUnitTest” and for triggering Javascript unit tests, you need to run Maven build with parameter “-P jsUnitTest”. Ideally, on your continous build system which has JsTestDriver integrated, you would want to use that profile in default build.

But how to integrate it into a continuous build system? Wait for part 12 (see the plan).

In the next part, I am going to write some view tests which are important often just ignored.

Understanding the Html5 for mobile – Part 3

In this part, I am going to optimize the Javascript for unit tests and I am going to convert the server side to REST.

Here is the plan:

Part1 App written completely in JSF and totally server-side
Source code

Article

Article (Turkish)
Part2 Converting to a Javascript client, using Servlets for web services
Source code

Article

Article (Turkish)

Part3
Optimizing the Javascript code for unit tests, using REST (used Apache CXF) for web
services


Source code



Article

Part4 Writing mock tests and running them with JsTestDriver
Source code

Article
Part5 Writing view tests
Source code

Article
Part6 Using local storage for caching the wordbook on modern browsers
Source code
Part7 Using WebSqlDatabase for caching the wordbook on modern browsers
Source code
Part8 Using offline cache for resources, making the application support running offline
Source code
Part9 Embedding the application in a Android WebView
Part10 Bundling the application with PhoneGap
Part11 Preparing for production and distribution
Part12 Integrating the JS unit tests with continuous build systems
Part13 Measuring Javascript code coverage with JsTestDriver

The Javascript code from the previous part is almost impossible to test, since only one class handles view, server connection and application logic. Now we’re gonna separate that class into 3:

  • “View” does the low level view operations. What I mean by low level is for example setting the inner Html of the elements and showing/hiding the elements.
  • “Manager” connects to the server and fetches the words.
  • “Controller” runs the application logic by using view and manager.
We”re not changing even one single line in our Html markup in this iteration.
artikelApp.AppController = function(appView, wordManager) {
    ...
    var init = function() {
        appView.registerAnswerDerButtonHandler(function(){answer('der');});
        appView.registerAnswerDieButtonHandler(function(){answer('die');});
        appView.registerAnswerDasButtonHandler(function(){answer('das');});
        appView.registerNextWordButtonHandler(next);

        appView.setScore(0);
        next();
    };

    var answer = function(article) {
        ...
    };

    var next = function() {
        ...
    };

    this.start = function(){
        appView.registerPageCreateHandler(init);
    };

};

Instead of doing low level view operations on controller, we’re delegating that to another class, “appView”. That is the necessary case for being able to mock the view layer and making sure that functionality of the view layer is called correctly.
...
    <script src="/resources/js/thirdparty/jquery-1.5.js"></script>

    <script src="/resources/js/thirdparty/jquery.mobile-1.0a4.1.js"></script>

    <script src="/resources/js/mine/config/context.js"></script>
    <script src="/resources/js/mine/config/applicationBeanFactory.js"></script>

    <script src="/resources/js/mine/view/appView.js"></script>
    <script src="/resources/js/mine/manager/wordManager.js"></script>
    <script src="/resources/js/mine/controller/appController.js"></script>
...
<script>
    new function(){
        var appView = new artikelApp.AppView();
        var appController = new artikelApp.AppController(appView, applicationBeanFactory.getWordManager());
        appController.start();
    }();
</script>
...

The controller is started at the bottom of the page and the controller is not polluting the global space.
Here you can see the controller is initialized with its dependencies : appView and wordManager. The interesting one is the wordManager.
WordManager is not instantiated directly, but using a factory. A Javascript IOC solution would also work, but it is an overkill for this application.
applicationBeanFactory is a global variable and it helps us creating the single instances of other “beans”.
artikelApp.ApplicationBeanFactory = function(){

    this.wordManager = null;

    ...

    this.getWordManager = function(){
        if(!this.wordManager){
            this.wordManager = new artikelApp.WordManager(instance.getContext());
        }
        return this.wordManager;
    };
    ...
};

var applicationBeanFactory = new artikelApp.ApplicationBeanFactory();

The dependency “wordManager” is there for keeping the application logic and the network operations separated.
Here is the complete implementation:
artikelApp.WordManager = function(context){

    this.getNextWord = function(callback){

        $.getJSON(context.getNextWordServiceUrl(), function(data) {

            if(!data || !data['word'])
                callback(null);

            var wordObject = data['word'];

            var word = wordObject['word'];
            var translation = wordObject['translation'];
            var article = wordObject['article'];

            callback(word, translation, article);

        }).error(function() {
            callback(null);
        });
    };

};

Here, since the Ajax call is async, we need to make our manager function async too. So, the controller needs to provide a “callback” and the callback is called when the word is fetched from the server:
var next = function() {
    appView.showLoadingDialog();
    var callback = function(word, translation, article) {
        if (word) {
            currentArticle = article;
            currentTranslation = translation;
            ...
            appView.setWord(word);
            ....
            appView.hideLoadingDialog();

        } else {      //then error occurred
            appView.hideLoadingDialog();
            alert("Unable to connect server, please check your internet connection.");
        }
    };
    wordManager.getNextWord(callback);
};

When the reponse is fetched from the server, the manager calls the callback and callback updates the UI using the “appView”.
The other dependency of the controller is “appView”, which does the low level UI operations.
artikelApp.AppView = function() {

    this.registerPageCreateHandler = function(callback){
        $(document).bind('pagecreate', function(){
            callback();
        });
    };

    ...

    this.setWord = function(word) {
        $('#word').html(word);
    };

    ....

    this.showResult = function() {
        $('#answers').hide();
        $('#article').show();
        $('#translation').show();
        $('#nextWordContainer').show();
    };
};

So, controller calls for example “setWord(…)” of the appView, instead of updating the UI directly.

Using REST – Apache CXF

Instead of doing the JSON encoding manually with a Servlet as in the previous part, we’d better use a REST service. I chose Apache CXF since I am an Apache guy (ASF committer), but of course other REST implementations (such as Jersey) is also fine.

I am not going to explain the necessary XML configuration or other configurations in order to keep my focus, but you can find the working example configured with Apache CXF at here.

The interface of the web service is defined as below.

...
import javax.ws.rs.GET;
import javax.ws.rs.Path;

public interface WordService {

    @GET
    @Path("/nextWord")
    public Word getNextWord();
}

In the interface, we’re defining the HTTP method as GET, and path to our “nextWord” service as “/nextWord”.

@Path("/wordService/")
@Scope("session")
@Produces("application/json")
public class WordServiceImpl implements WordService {
    ...
    @Override
    public Word getNextWord() {
        return getShuffledWords().pop();
    }
    private LinkedList<Word> getShuffledWords() {
    ...
    }
    ...
}

And in the implementation of that service, we don’t have to do anything for JSON encoding. Only

"@Produces("application/json")" 

annotation is enough for CXF to encode our returned “Word” object to JSON.

Of course, “Word” class must have some specialiaties to being converted to JSON:

@XmlRootElement(name = "word")
@XmlSeeAlso({Article.class})
public class Word implements Serializable {

    private String word;
    private String translation;
    private Article article;
    private Word() {}
    public Word(String word, String translation, Article article) {
    ...
    }
    ...
    @Override
    public boolean equals(Object o) {
    ...
    }
    @Override
    public int hashCode() {
    ...
    }
}

Now I am able to fetch the next word from the server using the URL

<SERVER_BASE_URL>/rest/mobileServices/wordService/nextWord

The part “/rest/mobileServices” comes from my configuration of CXF, which is not shown here.

In the next part, I am going to write some mock tests for controller.

Mobilde Html5 – Bölüm 2

Bu bölümde, ilk bölümde anlattığım uygulamayı %100 bir Javascript istemcisine çevirip kelime bilgilerini almak için basit bir JSON üreten Servlet yazacağım.

Bölüm 1 Uygulamanın tamamen JSF ile server-side yazılması
Kaynak kod

Makale

Türkçe Makale

Bölüm 2
Uygulamanın Javascript client’ına çevrilmesi ve web servisler için Servlet kullanılması

Kaynak kod



Makale



Türkçe Makale

Bölüm 3 Birim testler için Javascript optimizasyonu ve web servisler için REST (Apache CXF)
kullanımı

Kaynak kod

Makale
Bölüm 4 Mock testlerin yazımı ve JsTestDriver ile çalıştırılması
Kaynak kod

Makale
Bölüm 5 View testlerinin yazımı
Kaynak kod
Bölüm 6 Kelime veritabanını cache’lemek için Html5 local storage kullanımı
Kaynak kod
Bölüm 7 WebSqlDatabase ile kelime veritabanını cachle’leme
Kaynak kod
Bölüm 8 Sayfa kaynakları (CSS. JS. resimler) için ‘offline cache’ kullanımı ve uygulamayı offline
çalıştırma desteği

Kaynak kod
Bölüm 9 Uygulamayı Android WebView içine gömme
Bölüm 10 Uygulamayı PhoneGap ile paketleme
Bölüm 11 Dağıtım ve yayın için hazırlık
Bölüm 12 JS birim testlerini ‘continuous build’ sistemleri ile entegre etme
Bölüm 13 Javascript ‘code coverage’ ın JsTestDriver ile ölçülmesı

İlk olarak JSF sayfasını normal-statik bir Html sayfasına çevirelim. İlk bölümdeki JSF sayfası tamamen server taraflı bir çözümdü. Şimdi yapmak istediğim ise UI kısmını Javascript ile güncellemek. Bunun için ilk yapacağım şey tüm sayfayı normal ve statik Html ile tanımlamak olacak:

...
    <script src="/resources/js/thirdparty/jquery-1.5.js"></script>
    <script src="/resources/js/thirdparty/jquery.mobile-1.0a4.1.js"></script>
    <script src="/resources/js/mine/application.js"></script>
...
	<h4>Score : <span id="score"/></h4>
	    <h2>
		<span id="article"></span>
		<span id="word"></span>
	    </h2>
	<h4>
	    <span id="translation"></span>
	</h4>

	<div id="answers" class="ui-grid-b">
	    <div class="ui-block-a">
		<button id="answerDer">der</button>
	    </div>
...
	</div>
	<div id="nextWordContainer">
	    <button id="nextWord">Next</button>
	</div>
    </div>
</div>
<script>
    $(document).bind('pagecreate', function(){
        new artikelApp.Controller();
    });
</script>
...

Birinci bölüm ile karşılaştırıldığınızda, gördüğünüz gibi,  değişken kısımlara birer ID verdim. Örneğin “score”, “article”, “word” ve “translation”. Bu Html span elemanlarının içeriği Javascript ile değişecekler. Aynı zamanda butonlar için de IDler tanımladım : “answerDer”, “answerDie”, “nextWord”…Bu sayfayı Javascript kısmı olmadan browserda açtığınız zaman basit bir şablon göreceksiniz.Sonrasında, dokumana bir “pagecreate” event listener ekleniyor ve bu listener da Jquery Mobile sayfayı oluşturduktan sonra çağrılıyor. “onload” veya “onready” eventlerinin yerine Jquery Mobile’ın tetiklediği bu eventi kullanmamız gerekmekte. Bu listener uygulamamızı başlatacak. Burada Jquery Mobile tarafından tetiklenen eventleri görebilirsiniz.

artikelApp.Controller = function() {

    var currentTranslation = null;
    var currentArticle = null;
    var score = 0;

    var init = function() {

        $('#answerDer').bind('click', function() {
            answer('der');
        });
        $('#answerDie').bind('click', function() {
            answer('die');
        });
        $('#answerDas').bind('click', function() {
            answer('das');
        });
        $('#nextWord').bind('click', next);

        next();
    };

    var answer = function(article) {
    ...
    };

    var next = function() {
    ...
    };

    init();
};

Biraz önce bahsettiğim listener, uygulamayı başlatması için artikelApp.Controller sınıfının constructor’unu çağırıyor ve şunlar yapılıyor:

  • Cevap butonları (der, die, das) için listener ekleme
  • Gösterilecek sonraki kelimeyi (soruyu) “next” metodunu çağırarak server’dan çekme

Bu sınıfta gördüğünüz gibi tüm metotlar ve değişkenler constructor’ın scope’unda bulunmakta. Bu da detayları global scope’tan saklamaya yani global scope’u kirletmemeye yarıyor.Bunu açıklamak gerekirse; şöyle bir kod da yazabilirdik:

artikelApp.Controller = function() {

    this.currentTranslation = null;
    ....

    this.init = function() {
    ....
    };
    ....
};

ancak bu durumda metotlar ve değişkenler dışarıdan erişilebilir olacaktı, yani detaylar dışarıdan müdahaleye açık olacaktı (daha sonrasında birim testleri yazarken bunları dışarıdan erişilebilir yapacağız).

var next = function() {
  //show loading dialog
  $.mobile.pageLoading();

  //fetch next word
  $.getJSON('/wordService?'+new Date().getTime() , function(data) {
    console.log(&quot;data['article'] &quot; + data['article']);

    var word = data['word'];
    currentTranslation = data['translation'];
    currentArticle = data['article'];

    //show answer buttons, hide others
    $('#answers').show();
    $('#article').hide();
    $('#translation').hide();
    $('#nextWordContainer').hide();

    //set the content for the new word
    $('#word').html(word);
    $('#translation').html(currentTranslation);
    $('#article').html(currentArticle);

    //hide loading dialog
    $.mobile.pageLoading(true);
  }).error(function() {
    alert('Unable to connect server, please check your internet connection.');
    $.mobile.pageLoading(true);
  });
};

“next” metodu şunları yapacak:

  • Uzun sürebilecek işlemler yapacağımız için “Yükleniyor” diyaloğu gösterme
  • Sonraki kelimeyi (soruyu) server’dan çekme
  • UI’ı soru moduna dönüştürme (“next word” butonunu, cevabı, tercümeyi gizleme; cevap butonlarını gösterme ve soruyu güncelleme)
  • En son olarak “Yükleniyor” diyaloğunu gizleme

Jquery’nın getJSON(…) fonksiyonu server’a bağlanıp, veriyi okuyup daha sonra bu JSON formatındaki veriyi Javascript objesine çeviriyor. Daha sonrasında bu objeyi bize bir obje olarak asenkron callback metodunda veriyor.

Javascript tarafında bahsedeceğim son kısım “answer” metodu:

var answer = function(article) {

  if (article === currentArticle) {
    score++;
    $('#article').attr('class', 'correct');
  }
  else {
    score--;
    $('#article').attr('class', 'wrong');
  }

  $('#score').html(score);

  $('#answers').hide();
  $('#article').show();
  $('#translation').show();
  $('#nextWordContainer').show();
};

Gördüğünüz gibi,  kullanıcı cevap butonlarından birisine bastığında, “answer” metodu cevabı kontrol ediyor, skoru güncelliyor ve UI tarafını güncelliyor. UI tarafını güncelliyor demekten kastım ise cevap butonlarını gizleme, gösterilen skoru güncelleme ve basıldığında sonraki kelimeyi (soruyu) serverdan çekecek ve gösterecek olan “next” butonunu gösterme.

Server tarafına geçelim. JSON üreten Servlet çok temel bir Servlet. Her istekten sonra tüm kelimeleri alıp bir tanesini listeden çıkarıp, çıkardığı kelimeyi (tercümesini vs.) output’a JSON formatında yazıyor.

public class WordService extends HttpServlet{
    ...

    @Override
    public void init() throws ServletException {
    ...
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        final PrintWriter writer = resp.getWriter();

        final LinkedList<Word> shuffledWords =  getShuffledWords(req);
        final Word word = shuffledWords.pop();

        writer.printf("{ \"word\" : \"%s\", \"translation\" : \"%s\", \"article\" : \"%s\" }",
            word.getWord(), word.getTranslation(), word.getArticle().getText());

        writer.flush();
    }

    private LinkedList<Word> getShuffledWords(HttpServletRequest req) {
    ...
    }

}

Bu çok ilkel bir yöntem ve ilerleyen kısımlarda geliştirilecek. Bu Servlet’i şu şekilde kaydettirmemiz gerekiyor:

<servlet>
    <servlet-name>wordService</servlet-name>
    <servlet-class>tr.com.aliok.jsExperiments.service.WordService</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>wordService</servlet-name>
    <url-pattern>/wordService</url-pattern>
</servlet-mapping>

Artık server ile sadece data alışverişi yapan %100 Javascript istemcimiz var. Bu yöntem normal bir server-side uygulamadan daha iyi çünkü bize native bir uygulama gibi çalışan offline (çevrimdışı :) ) bir Html5 web uygulaması yapmanın önünü açıyor.

Sizin de bu aşamalardan geçerken yaşayacağınız problemlerden bahsetmek için ve bu aşamalarda çözümümüzün ne gibi sorunları olduğunu göstermek için yazmak istediğim “mükemmel” kodu bilerek yazmıyorum. Şu ana kadar:

  • Kodumuz test edilebilir değil : Jquery çağrıları ve uygulamanın mantık katmanı iç içe ve bu mock test yapmamızı imkansız hale getiriyor.  UI entegrasyon testleri için Selenium vs. gibi bir integration test mekanizması kullanılabilir, ama bunlar çok zahmetli işlemler. Bu yüzden mock testing yapmamız gerekiyor.
  • Herşey tek sınıfta yapılıyor : Kod parçalara ayrılmamış ve herşey iç içe.
  • Server tarafı gerçekten çok ilkel : Asla bu tarz bir yapıyı yöneticinize teklif etmeyin :)

Sonraki kısımda, bu sorunlara çözümleri anlatacağım ve çıkan yeni problemlerden bahsedeceğim.

Mobilde Html5 – Bölüm 1

Bu makale serisinde, bir Html5-Mobil uygulamayı nasıl yazabileceğimizi göstereceğim.

Tüm serinin kaynak kodunu şurada bulabilirsiniz : https://github.com/aliok/js-experiments-artikel .

Yazacağım uygılama çok basit bir Almanca ‘artikel’ (artikeller için buraya ve  buraya bkz.) öğrenme oyunu. Önyüz için Jquery Mobile kullanacağım, çünkü Jquery Mobile benim denediklerim arasındaki en gelişmiş framework.

Plan:


Bölüm 1
Uygulamanın tamamen JSF ile server-side yazılması

Kaynak kod



Makale



Türkçe Makale

Bölüm 2 Uygulamanın Javascript client’ına çevrilmesi ve web servisler için Servlet kullanılması
Kaynak kod

Makale

Türkçe Makale
Bölüm 3 Birim testler için Javascript optimizasyonu ve web servisler için REST (Apache CXF)
kullanımı

Kaynak kod

Makale
Bölüm 4 Mock testlerin yazımı ve JsTestDriver ile çalıştırılması
Kaynak kod

Makale
Bölüm 5 View testlerinin yazımı
Kaynak kod
Bölüm 6 Kelime veritabanını cache’lemek için Html5 local storage kullanımı
Kaynak kod
Bölüm 7 WebSqlDatabase ile kelime veritabanını cachle’leme
Kaynak kod
Bölüm 8 Sayfa kaynakları (CSS. JS. resimler) için ‘offline cache’ kullanımı ve uygulamayı offline
çalıştırma desteği

Kaynak kod
Bölüm 9 Uygulamayı Android WebView içine gömme
Bölüm 10 Uygulamayı PhoneGap ile paketleme
Bölüm 11 Dağıtım ve yayın için hazırlık
Bölüm 12 JS birim testlerini ‘continuous build’ sistemleri ile entegre etme
Bölüm 13 Javascript ‘code coverage’ ın JsTestDriver ile ölçülmesı

Bölüm1 – Uygulamanın tamamen JSF ile ‘server-side’ yazılması

Bu bölümde uygulama tamamen server tarafında çalışacak. Bu işi en iyi JSF’i bildiğim için JSF ile yapacağım. Siz de doğal olarak kendinize en kolay gelen teknolojiyi seçebilirsiniz. JSF’e veya herhangi bir framework’e zaten çok da bağımlı kalmayacağız. Ajax desteği olan herhangi bir framework işimizi görecektir.

Bu bölüm çok kolay. Yapılacaklar şunlar:

  1. Cevaplar için 3 tane düğme koymak
  2. Cevap butonuna basıldığında cevabı kontrol etmek
  3. Sonraki kelimeyi (soruyu) göstermek için bir buton koymak
  4. Sonraki kelime butonuna basılınca, sonraki kelimeyi almak

Bu bölüm makale serisinin aslı olmadığından ve oldukça basit olduğundan, çok fazla açıklamayacağım. Kodu şurada bulabilirsiniz : https://github.com/aliok/js-experiments-artikel/tree/master/part1.

Zor kısım Jquery Mobile’ı entegre etmek ama proje dokümantasyonu oldukça iyi.

...
<h:form>
  <h4><h:outputText value="#{text['score']} : #{wordController.score}"/></h4>
  <h2>
    <h:outputText value="#{wordController.currentWord.article.text} "
        rendered="#{wordController.answered}"
        styleClass="#{wordController.lastAnswerWasCorrect ? 'correct' : 'wrong'}"/>
    <h:outputText value="#{wordController.currentWord.word}"/>
  </h2>
  <h4><h:outputText value="#{wordController.currentWord.translation}"
        rendered="#{wordController.answered}"/></h4>
  <h:panelGroup rendered="#{not wordController.answered}">
    <div>
      <div>
        <h:commandButton actionListener="#{wordController.der}" value="#{text['der']}">
          <f:ajax render="@all" execute="@this" event="click" />
        </h:commandButton>
      </div>
...
    </div>
  </h:panelGroup>
  <h:panelGroup>
    <h:commandButton actionListener="#{wordController.nextWord}"
        value="#{text['next']}" rendered="#{wordController.answered}">
      <f:ajax render="@all" execute="@this" event="click" />
    </h:commandButton>
  </h:panelGroup>
</h:form>
…

Burada, cevap butonları (sadece birinin kodu gösteriliyor yukarıda) managed-bean’leri (bilmeyenler için server tarafındaki controller olarak özetleyebilirim) çağırıyor ve managed bean  cevabı kontrol ediyor. Daha sonrasında, butonda “render=”@all” ‘ attribute’u tanımlandığı için, tüm sayfa güncellenıyor ve sonuç kullanıcıya gösteriliyor.

Sonucun ve güncellenmiş skorun yanı sıra, ‘Sonraki’ butonu da gösteriliyor. Bu buton yine managed bean’i çağırmaya ve managed bean’in sonraki kelimeyi bir yerlerden (normalde veritabanından) almasına yarıyor. Yine ‘render’ attribute’u ‘@all’ olarak tanımlandığı için, sayfanın tamamı güncelleniyor ve sonraki kelime (aslında sonraki soru) gösteriliyor.

“ui-grid-b”, “ui-block-a” vs. class’ları Jquery mobile tarafından kullanılan özel tanımlamalar. Bu örnekte, “ui-grid-b” 3 sütunlu bir tablo (ya da ızgara) tanımlamaya yarıyor.  “ui-block-a” ise tanımlandığı div elemanının bu tablonun ilk sütunu olduğunu belirliyor.

...
public class WordController implements Serializable{
...
    private void answer(Article article){
        if(currentWord.getArticle().equals(article)){
            lastAnswerWasCorrect = true;
            score++;
        }
        else{
            lastAnswerWasCorrect = false;
            score--;
        }

        answered = true;
    }

    public void nextWord(ActionEvent event) {
        if(CollectionUtils.isEmpty(this.shuffledWords))
            constructWordSet();

        this.currentWord = this.shuffledWords.poll();

        answered = false;
    }

    public Word getCurrentWord(){
        if(this.currentWord==null)
            this.nextWord(null);

        return this.currentWord;
    }
...
}

Gördüğünüz gibi bu bölümde henüz Html5′e özel bir şey kullanmadım. Uygulama sıradan bir web uygulaması.

Web geliştirme ile uğraşmış her yazılımcının buradaki kodu kolayca anlayacağını düşünüyorum. İkinci makalemde bu uygulamayı bir %100 Javascript istemcisine çevireceğim. Ayrıca web servis için basit bir JSON üreten Servlet yazacağım.

Follow

Get every new post delivered to your Inbox.