2007-05-27

      最近做发财梦真的有些发疯了,把FCat Studio(森猫工作室)改名为HadronForce Software Studio(海创力量工作室),延续大学时代的创业梦想。设计了新的Logo还有主页草图。我不得不承认自己确实是个急性子,还没开始做什么就把域名申请了,http://www.hadronforce.com/,是有一点浮躁吧。
      Logo还算可以,主页就很难让自己满意,背景太过杂乱了。不过也没有时间再投入到这方面了,暂时如此吧。

  HadronForce Logo
      海创LOGO

HadronForce Home Page

海创Home Page

2007-05-27

     我今天在豆瓣上看到JSON讨论群中有人提问$.getJSON的使用方法,他不明白回调函数中json参数的用法。我对此做了简单的回答。
      其实,这个json对象其实我们都非常熟悉。
  不说那些难以理解的概念了,先简单说一下它的结构。
   -以左大括号({)开始
   -一个或多个属性,属性名和值之间用冒号隔开,属性之间使用逗号间隔
   -以右大括号(})结束
  对象中每个属性的值可以为任何JavaScript中允许的类型的数据,例如字符串、数组、数字、对象等。
   举一个简单的例子:
   {
   companyname:"SUN CO.",
   addresses:["address1","address2"],
   country:"USA",
   }
   很明显,其实JSON结构也就是一个哈希表结构。
   OK,了解了JSON的结构,我们就可以回来看一下JQuery中的$.getJSON()方法了。该方法有三个参数,$.getJSON(String url, Hash params, Function callback)。其中,url是获取JSON结构数据的请求地址,即访问该地址会得到一段JSON结构的文本(这里被JQuery封装了,这一段文本是通过请求对象的responseText属性得到的)。params是向url地址的服务发送的参数,服务器可以根据这些参数做出响应。参数的结构跟JSON的结构类似,都是形似“{key1:value1,key2:value2}”的结构。callback为回调函数,即function(json){//code...},在这个函数中做客户端处理。在callback函数中,参数json就是通过向url请求得来的JSON结构文本构造出来的对象。json参数的使用很简单,它是一个哈希表对象,可以通过json.key的方式来使用其中定义的属性。例如,如果我们传入的是前面所述例子的JSON结构,我们可以通过json.addresses[0]来获取addresses的第一个值。
   JSON不神秘,说白了其实就是个哈希表结构,在JavaScript中可以直接使用,非常方便。

2007-05-26
          关键字:AJAX 请求 并发 Firefox IE

    最近在开发中遇到一个棘手的问题,在FireFox浏览器中连续展开AJAX树(XTREE+XLoadTree)的多个节点会导致Hibernateorg.hibernate.exception.GenericJDBCExceptioncan't load entry异常(我们的系统用Ajax树控制数据节点,数据持久层采用Hibernate),但是在IE浏览器下进行同样的操作则不会抛任何异常。

        通过异常日志和代码分析发现,在这个异常后面隐藏着Hibernate Session的线程安全问题。当去掉引起该异常的代码时,线程安全的异常警告便会被显示出来。也就是说这个异常只是表象,其实真正导致系统异常的原因是Hibernate Session线程安全。当然,这个不能怪Hibernate,确实是我们的代码在存在并发时重写Hibernate Session导致冲突的可能。修正代码后问题得以解决。但令人费解的是,为什么在FireFox会导致Hibernate抛异常,在IE下却完全正常呢?

        两种浏览器都是发送Ajax请求到服务器,结果却大相径庭。虽说是服务器端的响应导致了该异常,但不难发现,只有当两个请求同时进行时才可能引起上述的Hibernate的线程安全问题,那么我们就必须假设这样的情况,当使用Firefox浏览器进行AJAX请求时同时发送两个甚至多个相同session id的请求线程到服务器,而IE仅发送一个。

        在网上找到一个非常有意思的测试,最终得出这样的结论:IE每次执行一个请求,Firefox一次执行两个请求。这一测试通过验证,结果属实。

三个文件,一个客户端(其中包含 prototype.js 文件):

 

下载: client.html

      <script type="text/javascript" src="prototype.js">script>      <script type="text/javascript" src="func.js">script> 

 

      <a href="javascript:ajax_get('A')">Click Aa>

      <hr />

      <a href="javascript:ajax_get('B')">Click Ba>

      <hr /> 

      <a href="javascript:ajax_get('C')">Click Ca>

      <hr />

      <div id="result">Result:<br />div>

 

一个 JS:

下载: func.js

     var i = 0;

     function ajax_get(action){  

         i++;

         var rand_num = Math.random();

         var myajax=new Ajax.Request("server.php?action=" + action + "&order=" + i +"&rand=" + rand_num,{method:'get',onComplete:show_result});

         $('result').innerHTML += get_date() + " , " + action + i + " : client post.
\n";

     }

     function show_result(response){

        var r_text = response.responseText;

        $('result').innerHTML += r_text;

     }

    function get_date(){

       var date = new Date();

      return date.getMinutes() + "m" + date.getSeconds();

     }

一个服务器端,PHP 文件:

下载: server.php

   

    //session_start();

     $action = $_GET['action'];

     $order = $_GET['order'];

     

     echo date("i\ms") . " , " . $action . $order . " : server is start.
\n";

     switch($action){

        case 'A':

            sleep(5);

            break;

       case 'B': 

           sleep(2);

           break; 

       case 'C':

           break;

     }

     echo date("i\ms") . " , " . $action . $order . " : server is end.
\n";

第一段代码是点 A,B,C 三个链接,分别发送不同的 action 的请求到服务器端。

第二段代码是接收到请求后,A 是等 5 秒后结束,B 是等 2 秒,C 是马上结束,用来模拟 PHP 执行代码的时间。

测试环境:IE6,FireFox,Opera,Windows XP,Apache 2,PHP 5.2.0

 

测试方法 1:

分别点 A,B,C,慢点点,等前一个回来后再点下一个:

结果:

IE6,FireFox 以及 Opera 结果都类似:

42m52 , A1 : client post.

42m52 , A1 : server is start.

42m57 , A1 : server is end.

42m57 , B1 : client post.

42m57 , B1 : server is start.

42m59 , B1 : server is end.

43m01 , C1 : client post.

43m01 , C1 : server is start.

43m01 , C1 : server is end.

结论:没啥惊讶的,点一个返回一个嘛。

测试方法 2:

分别点 A,C,快点点,不能前一个回来就点后面的,模拟并发:

结果和上面一样,恩,处理两个并发木问题。

测试方法 3:

分别点 A,B,C,快点点,不能前一个回来就点后面的,模拟并发:

结果:

IE6 和 Opera 结果类似:

51m30 , A1 : client post.

51m30 , B2 : client post.

51m30 , C3 : client post.

51m30 , C3 : server is start.

51m30 , C3 : server is end.

51m30 , B2 : server is start.

51m32 , B2 : server is end.

51m30 , A1 : server is start.

51m35 , A1 : server is end.

FireFox:

52m20 , A1 : client post.

52m20 , B2 : client post.

52m20 , C3 : client post.

52m20 , B2 : server is start.

52m22 , B2 : server is end.

52m22 , C3 : server is start.

52m22 , C3 : server is end.

52m20 , A1 : server is start.

52m25 , A1 : server is end.

结论:嘎嘎,有意思了吧,IE6 和 Opera 似乎不受影响,但 FireFox 的结果确是 C 要等 B 执行执行完毕后才开始(看时间,A 和 B 在服务器端都是 52m20 秒开始的,而 C 确是在 B 结束后的时间 5m22 开始执行的),这样可以排除服务器端 PHP 的影响,因为 IE,FireFox 以及 Opera 并没有改变服务器端什么,而返回的结果是不同的,所以 PHP 端顺序执行代码的结论应该是不成立的。

测试方法 4:

分别点 A,A,A,A,C 快点点,不能前一个回来就点后面的,模拟并发:

结果:

IE6:

56m06 , A1 : client post.

56m06 , A2 : client post.

56m06 , A3 : client post.

56m07 , A4 : client post.

56m07 , B5 : client post.

56m07 , B5 : server is start.

56m09 , B5 : server is end.

56m06 , A1 : server is start.

56m11 , A1 : server is end.

56m06 , A2 : server is start.

56m11 , A2 : server is end.

56m06 , A3 : server is start.

56m11 , A3 : server is end.

56m07 , A4 : server is start.

56m12 , A4 : server is end.

FireFox:

56m33 , A1 : client post.

56m33 , A2 : client post.

56m33 , A3 : client post.

56m34 , A4 : client post.

56m34 , B5 : client post.

56m33 , A1 : server is start.

56m38 , A1 : server is end.

56m33 , A2 : server is start.

56m38 , A2 : server is end.

56m38 , A3 : server is start.

56m43 , A3 : server is end.

56m38 , A4 : server is start.

56m43 , A4 : server is end.

56m43 , B5 : server is start.

56m45 , B5 : server is end.

Opera:

57m18 , A1 : client post.

57m19 , A2 : client post.

57m19 , A3 : client post.

57m19 , A4 : client post.

57m20 , B5 : client post.

57m18 , A1 : server is start.

57m23 , A1 : server is end.

57m19 , A2 : server is start.

57m24 , A2 : server is end.

57m19 , A3 : server is start.

57m24 , A3 : server is end.

57m19 , A4 : server is start.

57m24 , A4 : server is end.

57m23 , B5 : server is start.

57m25 , B5 : server is end.

结论:

从上面可以看出,IE6 似乎还是不受影响,而 FireFox 同时发送请求,但执行却是要两个两个执行, Opera 是四个四个执行。所以得到结论是 IE 可以同时发送无数个请求,FireFox 同时发送 2 个,Opera 是 4 个。

从网上查了一下,貌似是和浏览器的进程并发数有关。

更有趣的一个测试:

把上面的 PHP 文件的第 2 行的 session_start 的注释去掉,每次请求都打开一次 session,然后进行上面的测试。

测试方法 2:

IE6,FireFox 以及 Opera 结果类似:

15m03 , A1 : client post.

15m03 , C2 : client post.

15m08 , C2 : server is start.

15m08 , C2 : server is end.

15m03 , A1 : server is start.

15m08 , A1 : server is end.

结论:

貌似 session_start 也占了一个进程,A 和 C 同时发出去,但 C 要等 A 执行完后才开始执行。

测试方法 3:

分别点 A,B,C,快点点,不能前一个回来就点后面的,模拟并发:

IE6 和 FireFox 结果类似:

01m59 , A1 : client post.

01m59 , B2 : client post.

01m59 , C3 : client post.

01m59 , A1 : server is start.

02m04 , A1 : server is end.

02m04 , B2 : server is start.

02m06 , B2 : server is end.

02m06 , C3 : server is start.

02m06 , C3 : server is end.

Opera:

05m07 , A1 : client post.

05m08 , B2 : client post.

05m08 , C3 : client post.

05m07 , A1 : server is start.

05m12 , A1 : server is end.

05m14 , C3 : server is start.

05m14 , C3 : server is end.

05m12 , B2 : server is start.

05m14 , B2 : server is end.

结论:

从上面感觉 session 也占了浏览器的一个进程。从网上找了一下,说是 session 是发送一个 session ID 放到客户端,但我感觉既然是这么发送,应该不可能老占着一个进程啊。

2007-05-24

关键字:Tapestry4  单元测试  EasyMock  SpringMock

1        

Tapestry4中进行单元测试不是一件容易的事情。我们知道,一个完整的MVC框架内的单元测试,仅仅像纯粹简单java类那样测试是不够的。除了需要模拟出被测试类所依赖的类或接口实现以外,还要模拟必须的Servlet组件对象,以及Session等,较为繁琐。此外,由于Tapestry4采用了抽象类和抽象方法来实现页面,又给它的单元测试带来了不便。更糟糕的是,Tapestry4自身并未提供完整的单元测试解决方案,同时又欠缺第三方测试框架的支持,因此在互联网上有人抨击Tapestry4,“它的单元测试简直不可能”。

其实,如果我们对Tapestry的框架内部结构足够熟悉,如果对Servlet容器的内部机制足够了解,实现在Tapestry4上的单元测试不会太难,但是太多条件对于大多数Tapestry Programmer尚不具备,依我们目前对Tapestry4框架结构的了解程度,仍然无法实现像StrutsTapestry5那样完整的单元测试,但应该基本能够满足我们的测试需要。当然,随着对框架研究的逐步深入,这一情况应该会逐步得到改善。

接下来,本文将针对Tapestry4 + Hibernate + Spring框架组合中,Tapestry4的单元测试做简要说明。

 


2         工具介绍
2.1       EasyMock简介

EasyMock是一个Mock对象的类库。最新的EasyMock版本是2.2,可在http://sourceforge.net/projects/下载到。Mock 对象能够模拟领域对象的部分行为,并且能够检验运行结果是否和预期的一致。领域类将通过与Mock 对象的交互,来获得一个独立的测试环境。EasyMock可以动态地生成Mock 对象而不需要编写它们,因为也不会产生多余代码。说明:默认情况下,EasyMock只支持为接口生成Mock。如果还需要为类生成Mock,可在上述网址下载EasyMock的扩展包以完成此功能。EasyMock 2.2只能在Java 5.0以上的版本中运行。 

 

3         单元测试方法
3.1       方法一:EasyMock辅助方式 

大多数情况下,前端的单元测试需要与后端分离。而且为了测试方便,又会希望摆脱对Servlet容器的依赖。在这种情况下,以往的实现手段是手工编写所需的Mock对象来辅助完成测试,但这样会大幅增加测试代码编写量。幸运的是,这部分工作现在可以通过EasyMock来帮助我们完成,从而提高单元测试效率。

这种测试方式下,使用JUnitTestNG均可。

下面,以Metaone前端的登陆页面类(Login.java,为了增加例子可读性,代码已删减)的单元测试为例,讲解EasyMock辅助Tapestry4单元测试的方法。

被测试类Login.java的被测方法代码如下:

//Login.java

package com.carnation.metaone.page; 
import …… 
public abstract class Login extends BasePage implements PageBeginRenderListener {   
   // 用户名   
   public abstract String getUsername();    
   
// 密码   
  
public abstract String getPassword();    
  
public abstract UserService getUserService();    
  
public abstract HttpServletRequest getHttpServletRequest();    
   
public abstract User getUser();    
  
public abstract void setUser(User user);    
  
public abstract void setApplication(Application app);    
  
public abstract Application getApplication();    
  
// 登陆表单的监听方法   
  
public IPage loginSubmit(IRequestCycle cycle) {       
       
User user =
new User();
       user.setUsername(getUsername());
       user.setPassword(getPassword());
       Application application = getApplication();
       user.setApplication(application);
        try {
           user = getUserService().authenticate(user);
           user.setLocale(this.getLocale());
           user.setApplication(application);
           setUser(user);
           Date myDate = new Date();
           String ip = getHttpServletRequest().getRemoteAddr().trim();
            /*
            * 跳转页面,其中空URL代表当前应用的根目录
            */
           String url = "Home.html";
           if (application.getId().intValue()>1) {
              url = "application/Home.html";
           }
           return cycle.getPage(url);
       } catch (AuthenticationException e) {
           IValidationDelegate delegate = getDelegate();
           delegate.setFormComponent(null);
           delegate.record("认证失败,请重新登录!", null);
           return null;
       }
    }
测试代码编写步骤:
(1)                EasyMock包引入测试类,当然如果使用JDK1.5,可以使用静态引入,使用更加方便。
         import static org.easymock.EasyMock; //JDK1.5环境下可以静态引入
         import static org.easymock.EasyMock.createMock;
         import static org.easymock.EasyMock.expect;
         import static org.easymock.EasyMock.replay;
         import static org.easymock.EasyMock.verify; 

(2)                模拟后端Service接口和Servlet接口。
         UserService userService = createMock(UserService.class); 
         HttpServletRequest httpServletRequest = create(HttpServletRequest); 
         IRequestCycle cycle = create(IRequestCycle.class);
         Home home = create(Home.class); 

(3)                Login类实例化,并将需要初始化的变量进行初始化。Tapestry框架提供了一个org.apache.tapestry.test.Creator类,可以创建页面和组件抽象类的实例。
         Creator creator = new Creator();Map propertyMap =               new HashMap();propertyMap.add(“username”,”admin”);
         propertyMap.add(“password”,”admin”);
         propertyMap.add(“userService”,userService);
         Application application = new Application();
        
application.setId(new Long(“1”));
         
propertyMap.add(“application”,application);creator.newInstance(Login.class, propertyMap); 
(4)                编写Mock行为逻辑。EasyMock测试注重行为,即通过类似于录制、回放的过程来进行测试。在“录制”过程中,我们编写期望Mock对象发生的行为,例如返回值、被调用次数等。在回放过程中,EasyMock将自动检查实际发生行为与预期是否相符。那么,对应本例而言,代码如下:
     
expect(httpServletReques.getRemoteAddr()).andReturn(“192.168.0.1”);
        
try{
             
expect(userService.authenticate(not(eq(user)))).andReturn(getMockUser());
         
}catch(AuthenticationException e){   ……} 
        
expect(cycle.getPage(“application/Home.html”)).andReturn(home).anyTimes();
        
expect(cycle.getPage(“Home.html”)).andReturn(home).anyTimes();
       
 replay(httpServletReques);replay(userService);replay(cycle); 
        
Creator creator = new Creator();
        
Login loginPage = (Login) creator.newInstance(Login.class, propertyMap); 
(5)                断言
    
assert loginPage.loginSubmit(cycle).equals(home); 

3.2       方法二:SpringMock辅助方式(前后端集成)

在一些实际情况中,后端的一些方法内部对Hibernate Session进行操作,在这种情况下,对于那些无返回值的方法调用,在前端使用EasyMock是无法模拟的。

幸运的是,利用Spring Mock可以辅助这类测试。但需要注意,这并不意味这不再需要EasyMock Servlet接口的模拟仍然需要使用EasyMock来实现。

若采用这种方式进行测试,对测试工具的选择会有局限,仅支持JUnit,因为Spring Mock提供的基类继承自JUnit中的测试类。

该方式的实现步骤如下:

(1)    使测试类继承AbstractDependencyInjectionSpringContextTests

(2)    覆写getConfigLocations方法。以读取Spring上下文。

@Override

                    protected String[] getConfigLocations() {
         
                String[] str = new String[]{"applicationContext.xml"};                   
                         
return str;
                    
}

(3)    分别在onSetUp()onTearUp()方法中开启和关闭HibernateSession,这样延迟加载(Lazy Initialization)的集合也可以被正常调用。

                   private SessionFactory sf;

@Override
 protected void onSetUp() throws Exception {
                        
super.onSetUp();
                        
SessionFactory sessionFactory = (SessionFactory) applicationContext.getBean("sessionFactory");
                       
sf = sessionFactory;Session s = sessionFactory.openSession();
                       
TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(s));
                  
}
 
@Override
protected void onTearDown() throws Exception {
      
super.onTearDown();
       
SessionHolder holder = (SessionHolder)TransactionSynchronizationManager.getResource(sf);
       
Session s = holder.getSession();
       
s.flush();TransactionSynchronizationManager.unbindResource(sf);
        
SessionFactoryUtils.closeSessionIfNecessary(s, sf);} 

(4)    编写测试方法。与使用EasyMock不同,此时通过application对象的getBean()方法来获取Spring上下文中装配的Bean。例如

                    UserService userService =  (UserService)applicationContext.getBean("userService");

        通过上述两种方法,可以满足大部分单元测试需要,但仍然无法覆盖到界面操作方面的测试。这样的做法在理论上是可行的,但从实际角度出发,对于Web层质量的提升意义并不大,不如用Selenium做集成测试来的实在。:)

共1页 1