可不可不要这么样徘徊在目光内 / 你会察觉到我根本寂寞难耐 / 即使千多百个深夜曾在梦境内 / 我有吻过你这毕竟并没存在 / 人声车声开始消和逝 / 无声挣扎有个情感奴隶 / 是我多么的想她 / 但我偏偏只得无尽叹谓 / 其实每次见你我也着迷 / 无奈你我各有角色范围 / 就算在寂寞梦内超出好友关系 / 唯在暗里爱你暗里着迷 / 无谓要你惹上各种问题 / 共我道别吧别让空虚使我越轨 /

发布新日志

  • 用TestDirector生成的测试用例

    2007-09-11 10:30:37

    用TestDirector生成的测试用例有两种样式:Full Page和Tabular
            TestDirector中没有关于测试用例的目的以、该用例的前提条件等字段,因此可以在客户化时增加这些字段,由于客户化字段没有Memo类型,因此,可以将用例的目的和前提条件等在描述字段中进行描述,注意事项等也可以在此描述,如果有测试数据的话,可以在描述字段中对测试数据进行描述,具体的测试数据以文本或Excel方式保存,作为该测试用例的附件。
     
    样式一:Full Page
    1.1  用例名称 : 启动客户端程序
    路径 :
    主题 :服务程序
    设计状态 : Design
    设计者 : yuanhaisong
    创建日期 : 2002-06-11
    用例类型 : MANUAL
    描述 :目的:
       1. 检查服务程序能否以设计的五种方式正确启动客户端程序
          1.1. 菜单启动
          1.2. 快捷键启动
          1.3. 鼠标双击启动
          1.4. 定时启动
          1.5. 隔时启动
     
    前提条件:
       1. 服务程序已经运行;
       2. 客户端程序尚未运行;
     
    估计开发时间 : 0
    执行状态 : Passed
    Steps :
    Step Name : Step 1
    Descrīption :运行服务程序Server.exe
    Expected Result : 1. 服务程序运行;
    2. 服务程序在系统托盘中显示为图标;
    Step Name : Step 2
    Descrīption : 1. 在服务程序图标上单击右键;
    2. 在弹出的悬浮菜单中选择【启动在线升级程序】;
    Expected Result : 1. 弹出悬浮菜单,包括【启动在线升级程序】、【启动定时服务】、【启动隔时服务】和【关闭服务程序】四个菜单项;
    2.1 如果柜台系统需要更新,则弹出窗口提示是否更新,选择“Yes”按钮则启动客户端程序进行更新,选择“No”则关闭提示窗口;
    2.2 如果柜台系统不需要更新,则弹出窗口提示不需要更新,点击“OK”按钮关闭提示窗口;
    3 服务程序下载version.txt并生成DownLoadList.ini文件到服务端程序的安装目录下的 temp 目录中;
    4 version.txt文件的内容与服务器端的version.txt文件的内容一样;
    5 服务程序根据version.txt文件的内容与本地对应的文件进行比较,将需要升级的文件生成
    下载清单DownLoadList.ini
    6 如果需要更新并确定要更新,则下载更新文件并解压缩到Download目录中;
    Step Name : Step 3
    Descrīption :双击服务程序图标
    Expected Result : 1 如果柜台系统需要更新,则弹出窗口提示是否更新,选择“Yes”按钮则启动客户端程序进行更新,选择“No”则关闭提示窗口;
    2 如果柜台系统不需要更新,则弹出窗口提示不需要更新,点击“OK”按钮关闭提示窗口;
    3 服务程序下载version.txt并生成DownLoadList.ini文件到服务端程序的安装目录下的 temp 目录中;
    4 version.txt文件的内容与服务器端的version.txt文件的内容一样;
    5 服务程序根据version.txt文件的内容与本地对应的文件进行比较,将需要升级的文件生成下载清单DownLoadList.ini
    6 如果需要更新并确定要更新,则下载更新文件并解压缩到Download目录中;
    Step Name : Step 4
    Descrīption :按启动服务快捷键(Ctrl + F12)
    Expected Result : 1 如果柜台系统需要更新,则弹出窗口提示是否更新,选择“Yes”按钮则启动客户端程序进行更新,选择“No”则关闭提示窗口;
    2 如果柜台系统不需要更新,则弹出窗口提示不需要更新,点击“OK”按钮关闭提示窗口;
    3 服务程序下载version.txt并生成DownLoadList.ini文件到服务端程序的安装目录下的 temp 目录中;
    4 version.txt文件的内容与服务器端的version.txt文件的内容一样;
    5 服务程序根据version.txt文件的内容与本地对应的文件进行比较,将需要升级的文件生成下载清单DownLoadList.ini
    6 如果需要更新并确定要更新,则下载更新文件并解压缩到Download目录中;
    Step Name : Step 5
    Descrīption :修改配置文件Config.ini中的定时启动时间(SpecifyTime)
    Expected Result : 1. 到启动时间后,如果柜台系统需要更新,则弹出窗口提示是否更新,选择“Yes”按钮则启动客户端程序进行更新,选择“No”则关闭提示窗口;
    2. 到启动时间后,如果柜台系统不需要更新,则弹出窗口提示不需要更新,点击“OK”按钮关闭提示窗口;
    3 服务程序下载version.txt并生成DownLoadList.ini文件到服务端程序的安装目录下的 temp 目录中;
    4 version.txt文件的内容与服务器端的version.txt文件的内容一样;
    5 服务程序根据version.txt文件的内容与本地对应的文件进行比较,将需要升级的文件生成下载清单DownLoadList.ini
    6 如果需要更新并确定要更新,则下载更新文件并解压缩到Download目录中;
    Step Name : Step 6
    Descrīption :修改配置文件Config.ini中的隔时启动时间长度(IntervalTime)
    Expected Result : 1. 每间隔隔时启动时间长度后,如果柜台系统需要更新,则弹出窗口提示是否更新,选择“Yes”按钮则启动客户端程序进行更新,选择“No”则关闭提示窗口;
    2. 每间隔隔时启动时间长度后,如果柜台系统不需要更新,则弹出窗口提示不需要更新,点击“OK”按钮关闭提示窗口;
    3. 如果尚未对上一次的提示进行操作,而在此之间,升级状态发生了变化,到新一次隔时启动时间长度后,正确显示提示信息;
    4 服务程序下载version.txt并生成DownLoadList.ini文件到服务端程序的安装目录下的 temp 目录中;
    5 version.txt文件的内容与服务器端的version.txt文件的内容一样;
    6 服务程序根据version.txt文件的内容与本地对应的文件进行比较,将需要升级的文件生成下载清单DownLoadList.ini
    7 如果需要更新并确定要更新,则下载更新文件并解压缩到Download目录中;

    History :

    New Value
    Changer
    Change Time
    Change Date
    Field
    Passed
    yuanhaisong
    09:51
    2002-06-19
    执行状态
    No Run
    yuanhaisong
    09:35
    2002-06-19
    执行状态
    Not Completed
    yuanhaisong
    09:34
    2002-06-19
    执行状态
    Design
    yuanhaisong
    19:32
    2002-06-11
    设计状态

    样式二:Tabular
    1.1 用例名称 : 启动客户端程序
    路径 :
    主题 :服务程序
    设计状态 : Design
    设计者 : yuanhaisong
    创建日期 : 2002-06-11
    用例类型 : MANUAL
    描述 :目的:
       1. 检查服务程序能否以设计的五种方式正确启动客户端程序
          1.1. 菜单启动
          1.2. 快捷键启动
          1.3. 鼠标双击启动
          1.4. 定时启动
          1.5. 隔时启动
     
    前提条件:
       1. 服务程序已经运行;
       2. 客户端程序尚未运行;
     
    估计开发时间 : 0
    执行状态 : Passed
    Steps :

    Step Name
    Descrīption
    Expected Result
    Step 1
    运行服务程序Server.exe
    1. 服务程序运行;
    2. 服务程序在系统托盘中显示为图标;
    Step 2
    1. 在服务程序图标上单击右键;
    2. 在弹出的悬浮菜单中选择【启动在线升级程序】;
    1. 弹出悬浮菜单,包括【启动在线升级程序】、【启动定时服务】、【启动隔时服务】和【关闭服务程序】四个菜单项;
    2.1 如果柜台系统需要更新,则弹出窗口提示是否更新,选择“Yes”按钮则启动客户端程序进行更新,选择“No”则关闭提示窗口;
    2.2 如果柜台系统不需要更新,则弹出窗口提示不需要更新,点击“OK”按钮关闭提示窗口;
    3 服务程序下载version.txt并生成DownLoadList.ini文件到服务端程序的安装目录下的 temp 目录中;
    4 version.txt文件的内容与服务器端的version.txt文件的内容一样;
    5 服务程序根据version.txt文件的内容与本地对应的文件进行比较,将需要升级的文件生成下载清单DownLoadList.ini
    6 如果需要更新并确定要更新,则下载更新文件并解压缩到Download目录中;
    Step 3
    双击服务程序图标
    1 如果柜台系统需要更新,则弹出窗口提示是否更新,选择“Yes”按钮则启动客户端程序进行更新,选择“No”则关闭提示窗口;
    2 如果柜台系统不需要更新,则弹出窗口提示不需要更新,点击“OK”按钮关闭提示窗口;
    3 服务程序下载version.txt并生成DownLoadList.ini文件到服务端程序的安装目录下的 temp 目录中;
    4 version.txt文件的内容与服务器端的version.txt文件的内容一样;
    5 服务程序根据version.txt文件的内容与本地对应的文件进行比较,将需要升级的文件生成下载清单DownLoadList.ini
    6 如果需要更新并确定要更新,则下载更新文件并解压缩到Download目录中;
    Step 4
    按启动服务快捷键(Ctrl + F12
    1 如果柜台系统需要更新,则弹出窗口提示是否更新,选择“Yes”按钮则启动客户端程序进行更新,选择“No”则关闭提示窗口;
    2 如果柜台系统不需要更新,则弹出窗口提示不需要更新,点击“OK”按钮关闭提示窗口;
    3 服务程序下载version.txt并生成DownLoadList.ini文件到服务端程序的安装目录下的 temp 目录中;
    4 version.txt文件的内容与服务器端的version.txt文件的内容一样;
    5 服务程序根据version.txt文件的内容与本地对应的文件进行比较,将需要升级的文件生成下载清单DownLoadList.ini
    6 如果需要更新并确定要更新,则下载更新文件并解压缩到Download目录中;
    Step 5
    修改配置文件Config.ini中的定时启动时间(SpecifyTime
    1. 到启动时间后,如果柜台系统需要更新,则弹出窗口提示是否更新,选择“Yes”按钮则启动客户端程序进行更新,选择“No”则关闭提示窗口;
    2. 到启动时间后,如果柜台系统不需要更新,则弹出窗口提示不需要更新,点击“OK”按钮关闭提示窗口;
    3 服务程序下载version.txt并生成DownLoadList.ini文件到服务端程序的安装目录下的 temp 目录中;
    4 version.txt文件的内容与服务器端的version.txt文件的内容一样;
    5 服务程序根据version.txt文件的内容与本地对应的文件进行比较,将需要升级的文件生成下载清单DownLoadList.ini
    6 如果需要更新并确定要更新,则下载更新文件并解压缩到Download目录中;
    Step 6
    修改配置文件Config.ini中的隔时启动时间长度(IntervalTime
    1. 每间隔隔时启动时间长度后,如果柜台系统需要更新,则弹出窗口提示是否更新,选择“Yes”按钮则启动客户端程序进行更新,选择“No”则关闭提示窗口;
    2. 每间隔隔时启动时间长度后,如果柜台系统不需要更新,则弹出窗口提示不需要更新,点击“OK”按钮关闭提示窗口;
    3. 如果尚未对上一次的提示进行操作,而在此之间,升级状态发生了变化,到新一次隔时启动时间长度后,正确显示提示信息;
    4 服务程序下载version.txt并生成DownLoadList.ini文件到服务端程序的安装目录下的 temp 目录中;
    5 version.txt文件的内容与服务器端的version.txt文件的内容一样;
    6 服务程序根据version.txt文件的内容与本地对应的文件进行比较,将需要升级的文件生成下载清单DownLoadList.ini
    7 如果需要更新并确定要更新,则下载更新文件并解压缩到Download目录中;

    History :
    New Value
    Changer
    Change Time
    Change Date
    Field
    Passed
    yuanhaisong
    09:51
    2002-06-19
    执行状态
    No Run
    yuanhaisong
    09:35
    2002-06-19
    执行状态
    Not Completed
    yuanhaisong
    09:34
    2002-06-19
    执行状态
    Design
    yuanhaisong
    19:32
    2002-06-11
    设计状态
     


     

  • 对DAO编写单元测试

    2007-08-21 23:19:44

        单元测试作为保证软件质量及重构的基础,早已获得广大开发人员的认可。单元测试是一种细粒度的测试,越来越多的开发人员在提交功能模块时也同时提交相应的单元测试。对于大多数开发人员来讲,编写单元测试已经成为开发过程中必须的流程和最佳实践。

            对普通的逻辑组件编写单元测试是一件容易的事情,由于逻辑组件通常只需要内存资源,因此,设置好输入输出即可编写有效的单元测试。对于稍微复杂一点的组件,例如Servlet,我们可以自行编写模拟对象,以便模拟HttpRequest和HttpResponse等对象,或者,使用EasyMock之类的动态模拟库,可以对任意接口实现相应的模拟对象,从而对依赖接口的组件进行有效的单元测试。

            在J2EE开发中,对DAO组件编写单元测试往往是一件非常复杂的任务。和其他组件不通,DAO组件通常依赖于底层数据库,以及JDBC接口或者某个ORM框架(如Hibernate),对DAO组件的测试往往还需引入事务,这更增加了编写单元测试的复杂性。虽然使用EasyMock也可以模拟出任意的JDBC接口对象,或者ORM框架的主要接口,但其复杂性往往非常高,需要编写大量的模拟代码,且代码复用度很低,甚至不如直接在真实的数据库环境下测试。不过,使用真实数据库环境也有一个明显的弊端,我们需要准备数据库环境,准备初始数据,并且每次运行单元测试后,其数据库现有的数据将直接影响到下一次测试,难以实现“即时运行,反复运行”单元测试的良好实践。

            本文针对DAO组件给出一种较为合适的单元测试的编写策略。在JavaEE开发网的开发过程中,为了对DAO组件进行有效的单元测试,我们采用HSQLDB这一小巧的纯Java数据库作为测试时期的数据库环境,配合Ant,实现了自动生成数据库脚本,测试前自动初始化数据库,极大地简化了DAO组件的单元测试的编写。
            在Java领域,JUnit作为第一个单元测试框架已经获得了最广泛的应用,无可争议地成为Java领域单元测试的标准框架。本文以最新的JUnit 4版本为例,演示如何创建对DAO组件的单元测试用例。

            JavaEEdev的持久层使用Hibernate 3.2,底层数据库为MySQL。为了演示如何对DAO进行单元测试,我们将其简化为一个DAOTest工程:
    对DAO编写单元测试 图-1

            由于将Hibernate的Transaction绑定在Thread上,因此,HibernateUtil类负责初始化SessionFactory以及获取当前的Session:

     
     public class HibernateUtil {
    private static final SessionFactory sessionFactory;
    static {
    try {
    sessionFactory = new AnnotationConfiguration()
    .configure()
    .buildSessionFactory();
    }
    catch(Exception e) {
    throw new ExceptionInInitializerError(e);
    }
    } ? public static Session getCurrentSession() {
    return sessionFactory.getCurrentSession();
    }
    }

      HibernateUtil还包含了一些辅助方法,如:

     public static Object query(Class clazz, Serializable id);
    public static void createEntity(Object entity);
    public static Object queryForObject(String hql, Object[] params);
    public static List queryForList(String hql, Object[] params);

            在此不再多述。

    实体类User使用JPA注解,代表一个用户:

      @Entity
    @Table(name="T_USER")
    public class User {
    public static final String REGEX_USERNAME = "[a-z0-9][a-z0-9\\-]{1,18}[a-z0-9]";
    public static final String REGEX_PASSWORD = "[a-f0-9]{32}";
    public static final String REGEX_EMAIL = "([0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\\w]*[0-9a-zA-Z]\\.)+[a-zA-Z]{2,9})"; private String username;???? // 用户名
    private String password;???? // MD5口令
    private boolean admin;?????? // 是否是管理员
    private String email;?? ?????// 电子邮件
    private int emailValidation; // 电子邮件验证码
    private long createdDate;??? // 创建时间
    private long lockDate;?????? // 锁定时间 public User() {} public User(String username, String password, boolean admin, long lastSignOnDate) {
    this.username = username;
    this.password = password;
    this.admin = admin;
    } @Id
    @Column(updatable=false, length=20)
    @Pattern(regex=REGEX_USERNAME)
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; } @Column(nullable=false, length=32)
    @Pattern(regex=REGEX_PASSWORD)
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; } @Column(nullable=false, length=50)
    @Pattern(regex=REGEX_EMAIL)
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; } @Column(nullable=false)
    public boolean getAdmin() { return admin; }
    public void setAdmin(boolean admin) { this.admin = admin; } @Column(nullable=false, updatable=false)
    public long getCreatedDate() { return createdDate; }
    public void setCreatedDate(long createdDate) { this.createdDate = createdDate; } @Column(nullable=false)
    public int getEmailValidation() { return emailValidation; }
    public void setEmailValidation(int emailValidation) { this.emailValidation = emailValidation; } @Column(nullable=false)
    public long getLockDate() { return lockDate; }
    public void setLockDate(long lockDate) { this.lockDate = lockDate; } @Transient
    public boolean getEmailValidated() { return emailValidation==0; } @Transient
    public boolean getLocked() {
    return !admin && lockDate>0 && lockDate>System.currentTimeMillis();
    }
    }

      实体类PasswordTicket代表一个重置口令的请求:

      @Entity
    @Table(name="T_PWDT")
    public class PasswordTicket {
    private String id;
    private User user;
    private String ticket;
    private long createdDate; @Id
    @Column(nullable=false, updatable=false, length=32)
    @GeneratedValue(generator="system-uuid")
    @GenericGenerator(name="system-uuid", strategy="uuid")
    public String getId() { return id; }
    protected void setId(String id) { this.id = id; } @ManyToOne
    @JoinColumn(nullable=false, updatable=false)
    public User getUser() { return user; }
    public void setUser(User user) { this.user = user; } @Column(nullable=false, updatable=false, length=32)
    public String getTicket() { return ticket; }
    public void setTicket(String ticket) { this.ticket = ticket; } @Column(nullable=false, updatable=false)
    public long getCreatedDate() { return createdDate; }
    public void setCreatedDate(long createdDate) { this.createdDate = createdDate; }
    }

      UserDao接口定义了对用户的相关操作:

     public interface UserDao {
    User queryForSignOn(String username);
    User queryUser(String username);
    void createUser(User user);
    void updateUser(User user);
    boolean updateEmailValidation(String username, int ticket);
    String createPasswordTicket(User user);
    boolean updatePassword(String username, String oldPassword, String newPassword);
    boolean queryResetPassword(User user, String ticket);
    boolean updateResetPassword(User user, String ticket, String password);
    void updateLock(User user, long lockTime);
    void updateUnlock(User user);
    }

      UserDaoImpl是其实现类:

     public class UserDaoImpl implements UserDao {
    public User queryForSignOn(String username) {
    User user = queryUser(username);
    if(user.getLocked())
    throw new LockException(user.getLockDate());
    return user;
    }  public User queryUser(String username) {
    return (User) HibernateUtil.query(User.class, username);
    }  public void createUser(User user) {
    user.setEmailValidation((int)(Math.random() * 1000000) + 0xf);
    HibernateUtil.createEntity(user);
    }
    // 其余方法略
    ...
    }

      由于将Hibernate事务绑定在Thread上,因此,实际的客户端调用DAO组件时,还必须加入事务代码:

     Transaction tx =  HibernateUtil.getCurrentSession().beginTransaction();
    try {
    dao.xxx();
    tx.commit();
    }
    catch(Exception e) {
    tx.rollback();
    throw e;
    }
     下面,我们开始对DAO组件编写单元测试。前面提到了HSQLDB这一小巧的纯Java数据库。HSQLDB除了提供完整的JDBC驱动以及事务支持外,HSQLDB还提供了进程外模式(与普通数据库类似)和进程内模式(In-Process),以及文件和内存两种存储模式。我们将HSQLDB设定为进程内模式及仅使用内存存储,这样,在运行JUnit测试时,可以直接在测试代码中启动HSQLDB。测试完毕后,由于测试数据并没有保存在文件上,因此,不必清理数据库。 

      此外,为了执行批量测试,在每个独立的DAO单元测试运行前,我们都执行一个初始化脚本,重新建立所有的表。该初始化脚本是通过HibernateTool自动生成的,稍后我们还会讨论。下图是单元测试的执行顺序:
    对DAO编写单元测试 图-2?

      在编写测试类之前,我们首先准备了一个TransactionCallback抽象类,该类通过Template模式将DAO调用代码通过事务包装起来:

     public abstract class TransactionCallback {
    public final Object execute() throws Exception {
    Transaction tx = HibernateUtil.getCurrentSession().beginTransaction();
    try {
    Object r = doInTransaction();
    tx.commit();
    return r;
    }
    catch(Exception e) {
    tx.rollback();
    throw e;
    }
    }
    // 模板方法:
    protected abstract Object doInTransaction() throws Exception;
    }

      其原理是使用JDK提供的动态代理。由于JDK的动态代理只能对接口代理,因此,要求DAO组件必须实现接口。如果只有具体的实现类,则只能考虑CGLIB之类的第三方库,在此我们不作更多讨论。

      下面我们需要编写DatabaseFixture,负责启动HSQLDB数据库,并在@Before方法中初始化数据库表。该DatabaseFixture可以在所有的DAO组件的单元测试类中复用:

     public class DatabaseFixture {
    private static Server server = null; // 持有HSQLDB的实例
    private static final String DATABASE_NAME = "javaeedev"; // 数据库名称
    private static final String SCHEMA_FILE = "schema.sql"; // 数据库初始化脚本
    private static final List<String> initSqls = new ArrayList<String>();  @BeforeClass // 启动HSQLDB数据库
    public static void startDatabase() throws Exception {
    if(server!=null)
    return;
    server = new Server();
    server.setDatabaseName(0, DATABASE_NAME);
    server.setDatabasePath(0, "mem:" + DATABASE_NAME);
    server.setSilent(true);
    server.start();
    try {
    Class.forName("org.hsqldb.jdbcDriver");
    }
    catch(ClassNotFoundException cnfe) {
    throw new RuntimeException(cnfe);
    }
    LineNumberReader reader = null;
    try {
    reader = new LineNumberReader(new    InputStreamReader(DatabaseFixture.class.getClassLoader().getResourceAsStream(SCHEMA_FILE)));
    for(;;) {
    String line = reader.readLine();
    if(line==null) break;
    // 将text类型的字段改为varchar(2000),因为HSQLDB不支持text:
    line = line.trim().replace(" text ", " varchar(2000) ").replace("  text,", " varchar(2000),");
    if(!line.equals(""))
    initSqls.add(line);
    }
    }
    catch(IOException e) {
    throw new RuntimeException(e);
    }
    finally {
    if(reader!=null) {
    try { reader.close(); } catch(IOException e) {}
    }
    }
    }  @Before // 执行初始化脚本
    public void initTables() {
    for(String sql : initSqls) {
    executeSQL(sql);
    }
    } static Connection getConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:hsqldb:mem:" + DATABASE_NAME, "sa", "");
    } static void close(Statement stmt) {
    if(stmt!=null) {
    try {
    stmt.close();
    }
    catch(SQLException e) {}
    }
    } static void close(Connection conn) {
    if(conn!=null) {
    try {
    conn.close();
    }
    catch(SQLException e) {}
    }
    } static void executeSQL(String sql) {
    Connection conn = null;
    Statement stmt = null;
    try {
    conn = getConnection();
    boolean autoCommit = conn.getAutoCommit();
    conn.setAutoCommit(true);
    stmt = conn.createStatement();
    stmt.execute(sql);
    conn.setAutoCommit(autoCommit);
    }
    catch(SQLException e) {
    log.warn("Execute failed: " + sql + "\nException: " + e.getMessage());
    }
    finally {
    close(stmt);
    close(conn);
    }
    } public static Object createProxy(final Object target) {
    return Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    new InvocationHandler() {
    public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
    return new TransactionCallback() {
    @Override
    protected Object doInTransaction() throws Exception {
    return method.invoke(target, args);
    }
    }.execute();
    }
    }
    );
    }
    }

      注意DatabaseFixture的createProxy()方法,它将一个普通的DAO对象包装为在事务范围内执行的代理对象,即对于一个普通的DAO对象的方法调用前后,自动地开启事务并根据异常情况提交或回滚事务。

      下面是UserDaoImpl的单元测试类:

     public class UserDaoImplTest extends DatabaseFixture {
    private UserDao userDao = new UserDaoImpl();
    private UserDao proxy = (UserDao)createProxy(userDao);  @Test
    public void testQueryUser() {
    User user = newUser("test");
    proxy.createUser(user);
    User t = proxy.queryUser("test");
    assertEquals(user.getEmail(), t.getEmail());
    }
    }

      注意到UserDaoImplTest持有两个UserDao引用,userDao是普通的UserDaoImpl对象,而proxy则是将userDao进行了事务封装的对象。

      由于UserDaoImplTest从DatabaseFixture继承,因此,@Before方法在每个@Test方法调用前自动调用,这样,每个@Test方法执行前,数据库都是一个经过初始化的“干净”的表。

      对于普通的测试,如UserDao.queryUser()方法,直接调用proxy.queryUser()即可在事务内执行查询,获得返回结果。

      对于异常测试,例如期待一个ResourceNotFoundException,就不能直接调用proxy.queryUser()方法,否则,将得到一个UndeclaredThrowableException:

      对DAO编写单元测试 图-3

      这是因为通过反射调用抛出的异常被代理类包装为UndeclaredThrowableException,因此,对于异常测试,只能使用原始的userDao对象配合TransactionCallback实现:

     @Test(expected=ResourceNotFoundException.class)
    public void testQueryNonExistUser() throws Exception {
    new TransactionCallback() {
    protected Object doInTransaction() throws Exception {
    userDao.queryUser("nonexist");
    return null;
    }
    }.execute();
    }

      到此为止,对DAO组件的单元测试已经实现完毕。下一步,我们需要使用HibernateTool自动生成数据库脚本,免去维护SQL语句的麻烦。相关的Ant脚本片段如下:

     <target name="make-schema" depends="build"  descrīption="create schema">
    <taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask">
    <classpath refid="build-classpath"/>
    </taskdef>
    <taskdef name="annotationconfiguration" classname="org.hibernate.tool.ant.AnnotationConfigurationTask">
    <classpath refid="build-classpath"/>
    </taskdef>
    <annotationconfiguration configurationfile="${src.dir}/hibernate.cfg.xml"/>
    <hibernatetool destdir="${gen.dir}">
    <classpath refid="build-classpath"/>
    <annotationconfiguration configurationfile="${src.dir}/hibernate.cfg.xml"/>
    <hbm2ddl
    export="false"
    drop="true"
    create="true"
    delimiter=";"
    ōutputfilename="schema.sql"
    destdir="${src.dir}"
    />
    </hibernatetool>
    </target>

      完整的Ant脚本以及Hibernate配置文件请参考项目工程源代码。

      利用HSQLDB,我们已经成功地简化了对DAO组件进行单元测试。我发现这种方式能够找出许多常见的bug:

    • HQL语句的语法错误,包括SQL关键字和实体类属性的错误拼写,反复运行单元测试就可以不断地修复许多这类错误,而不需要等到通过Web页面请求而调用DAO时才发现问题;
    • 传入了不一致或者顺序错误的HQL参数数组,导致Hibernate在运行期报错;
    • 一些逻辑错误,包括不允许的null属性(常常由于忘记设置实体类的属性),更新实体时引发的数据逻辑状态不一致。

      总之,单元测试需要根据被测试类的实际情况,编写最简单最有效的测试用例。本文旨在给出一种编写DAO组件单元测试的有效方法。

  • 代替测试用例的检查表-但不要作为偷懒的借口

    2007-08-15 17:35:37

    代替测试用例的检查表-但不要作为偷懒的借口

    字体:        | 上一篇 下一篇 | 打印  | 我要投稿

      2004年底在大连出差的时候,帮一个项目做测试,顺便写下这个检查表,这个检查表对测试的初学者积累经验比较有用,实际对于有经验的测试人员尤其对于测试业务管理信息系统,基本上大量的测试不需要再编写测试用例,当然对业务流程、复杂逻辑还是要设计详细的测试用例的。如果你测试的系统是有大量人机交互的业务管理信息系统,而且你又比较懒惰,那就可以使用这个检查表检查了。
      因此我总结了这类系统中常用的测试的检查项,供当时项目组的测试人员使用,现在再次整理出来发于博客。
    1        针对测试组长或测试经理
    1.1   测试管理工作检查表:
    1.      检查每轮测试开始时测试环境是否准备好(包括软件硬件、测试基本数据等);
    2.      确保测试环境(数据和程序)与开发分离,除了测试组之外其他人不能更新测试环境的数据和程序;
    3.      每轮测试根据上一轮的情况和总体测试计划做分工调整;
    4.      检查case库的填报情况,抽查执行过的case;
    5.      检查BUG提交情况,抽查提交的BUG是否规范;
    6.      每天晚上统计BUG情况,填写每天的BUG报告;
    7.      根据每天的测试情况,决定是否开发组要发布新的BUILD;
    8.      每轮测试结束后填写测试总结。
    2        下面是针对测试执行人员的:
    2.1   输入、编辑功能的验证检查点:
    1.       必输项是否有红星标记,如果不输入提示是否跟相应的Label对应,提示的顺序是否跟Form输入域的排列次序一致;
    2.       输入的特殊字符是否能正确处理:`~!@#$%^&*()_+-={}[]|\:;”’<>,./?;
    3.       Form下拉菜单的值是否正确,下拉菜单的值通过维护后是否正确显示并可用;下拉菜单比如是机构编码,要到机构编码的维护界面查询一下是否Form列出的与其一致;
    4.       涉及到下拉菜单的编辑修改Form,要检查在编辑和修改From中,下拉菜单是否能正确显示当前值;
    5.       Form提交后,要逐项检查输入的内容跟通过查询的结果一致;
    6.       有多层下拉菜单选择的情况要校验两层菜单的选择是否正确,比如:
    a)       部门 人员
           部门 财务软件开发部人员 张三
    7.       备注字段的超常检查;
    8.       提交保存后能否转到合适的页面;
    9.       编辑Form显示的数据是否跟该记录的实际数据一致;
    10.    编辑权限的检查,比如:user1的数据user2不能编辑等;
    11.    可编辑数据项的检查,比如:数据在正式提交之前所有的属性都可以编辑,在提交之后,编号、状态等不能编辑,要根据业务来检查是否符合需求;
    12.    对于保存有事务Trasaction提交,比如一次提交对多表插入操作,要检查事务Trasaction的处理,保证数据的完整和一致;
    13.    其他的合法性校验。
    2.2   查询功能检查点:
    1.       查询输入Form是否正常工作,不输入数据是否查询到全部记录;
    2.       当查询的数据非常多的时候,性能有无问题;
    3.       查询的下拉菜单列出的数据是否正确;
    4.       查询结果是否正确;对于复杂的查询要通过SQL来检查结果;
    5.       如输入%*?等统配符是否会导致查询错误;
    6.       查询结果列表分页是否正确,在点击下一页上一页时,查询条件是否能带过去,不能点击翻页时又重新查询;
    7.       对于数据量比较大的表查询时,不容许无条件查询,避免性能问题的出现;
    8.       对于查询输入项的值是固定的要用下拉菜单,比如状态、类型等;
    9.       分页的统计数字是否正确,共X页,第N页,共X条记录等;
    10.    对于查询有统计的栏目,比如:总计、合计等要计算数据是否正确;
    11.    查询结果有超链接的情况要检查超链接是否正确;
    12.    查询权限的检查,比如:user1不能查询到user2的数据等;
    2.3   删除功能检查点:
    1.       必须有“确认删除”的提示;
    2.       根据需求检查是软删除还是硬删除,来检查数据库中是否还存在该条记录;
    3.       是否有相关的数据删除,如果有要确认该相关的数据也已经删除,并且在同一事务中完成;
    4.       是否有删除约束,如果有删除约束,要检查该记录是否被约束,如果被约束该记录不能被删除;
    5.       如果是软删除,用查询、统计界面检查该条记录能否被查询出来,数据是否被统计进去;
    6.       检查因为业务约束不能删除的数据能否被保护不能手工删除,比如:流程中已经审批的文件不能被删除;
    7.       跟删除相关的权限问题,比如:需求要求只有管理员和该记录的创建人能够删除该记录,那就以不同的用户和角色登录进去,执行删除操作,检查是否与需求匹配;
    2.4   上传附件检查点:
    1.       检查是否能正确上传附件文件;
    2.       检查上传的文件是否能正确下载并打开;
    3.       至少检查下列大小的文件能正确上传,100k,1M,2M,4M,10M,20M等;
    4.       如果没有指定类型的限制,至少上传以下几种类型的文件能否正确上传并正确打开,类型有:.doc, .xls, .txt, .ppt, .htm, .gif, .jpg, .bmp, .tif, .avi等;
    5.       如果有文件类型的限制还要检查能上传的文件的类型;
    6.       上传同名的文件,在打开的时候是否出错;
    7.       有中文文件名的文件能否正确上传;
    2.5   影响操作性能的检查点:
    (不能代替系统的性能测试和压力测试,主要看系统在正常操作情况下的响应和处理能力)
    1.       对数据记录条数比较多的表的查询操作,避免全表查询,比如对银行用户账号的查询就不能缺省全部查出,必须让用户输入查询条件;
    2.       菜单树,测试大量数据时菜单树的响应情况;
    3.       有日志的查询或者统计,要注意查询的效率;
    4.       大报表的处理或者批处理的操作,要关注效率,比如:银行对帐、财务年终结算、财务年报表、系统初始化等;
    5.       大报表的排序sort、组函数的使用等;
    6.       大数据量的处理,如导入、导出、系统备份、文件传输等;
  • 2007-01-10 | 工作中测试用例总结

    2007-07-07 00:55:09

    2007-01-10 | 工作中测试用例总结

    2007-04-27 21:51:19 / 个人分类:测试用例

    测试用例设计总结:
    测试用例
    测试用例是指对一项特定的软件产品进行测试任务的描述,体现测试方案、方法、技术和策略。内容包括测试目标、测试环境、输入数据、测试步骤、预期结果、测试脚本等,并形成文档。需要把把测试数据和测试脚本从测试用例中划分出来。测试用例更趋于是针对软件产品的功能、业务规则和业务处理所设计的测试方案。对软件的每个特定功能或运行操作路径的测试构成了一个个测试用例。
    二.测试用例的设置
    1.按功能测试是最简捷的,按用例规约遍历测试每一功能。
    2.路径分析是一个很好的方法,对于复杂操作的程序模块,其各功能的实施是相互影响、紧密相关、环环相扣的,可以演变出数量繁多的变化。没有严密的逻辑分析,产生遗漏是在所难免。其最大的优点是在于可以避免漏测试。
    三.设计测试用例方法
    测试用例可以分为基本事件、备选事件和异常事件。
    1.基本事件的测试用例应包含所有需要实现的需求功能,覆盖率达100%。从需求和设计中所有基本实现的功能覆盖。用于证明该需求已经满足,通常称作正面测试用例。
    2.设计备选事件和异常事件的用例,则要复杂和困难得多。往往在设计编码阶段形成的文档对备选事件和异常事件分析描述不够详尽。而测试本身则要求验证全部非基本事件,并同时尽量发现其中的软件缺陷。反映某个无法接受、反常或意外的条件或数据,用于论证只有在所需条件下才能够满足该需求,这个测试用例称作负面测试用例。
    四.测试常用的基本方法
    等价类划分法、边界值分析法、错误推测法、因果图法、逻辑覆盖法等设计测试用例。
    步骤1:首先使被测单元运行(模块设计导出的测试,对等区间划分),如果这个都有问题,可以直接把版本打回去
    步骤2:正面测试(Positive Testing) 正面测试的测试用例用于验证被测单元能够执行应该完成的工作。测试设计者应该查阅相关的设计说明;每个测试用例应该测试模块设计说明中一项或多项陈述。如果涉及多个设计说明,最好使测试用例的序列对应一个模块单元的主设计说明。(设计说明导出的测试,对等区间划分,状态转换测试)
    步骤3:负面测试(Negative Testing)负面测试用于验证软件不执行其不应该完成的工作。这一步骤主要依赖于错误猜测,需要依靠测试设计者的经验判断可能出现问题的位置。(错误猜测,边界值分析,内部边界值测试,状态转换测试)
    步骤4:设计需求中其它测试特性用例设计,如果需要,应该针对性能、余量、安全需要、保密需求等设计测试用例。
    步骤5:执行测试用例
    五.测试用例的实施
    1.在实施测试时测试用例作为测试的标准,但测试人员可以在实施过程中添加其他有效的测试用例。并对测试情况记录在测试用例管理软件(TD)中,以便自动生成测试结果文档。
    2.测试数据和测试用例是分离的,除正常数据之外,还必须根据测试用例设计大量边缘数据和错误数据。
    3.测试用例在形成文档后也还需要不断完善。主要来自三方面的缘故:第一、在测试过程中发现设计测试用例时考虑不周,需要完善;第二、在软件交付使用后反馈的软件缺陷,而缺陷又是因测试用例存在漏洞造成;第三、软件自身的新增功能以及软件版本的更新,测试用例也必须配套修改更新。
    六.测试用例设计及执行经验总结
    1.通常应该避免依赖先前测试用例的输出。虽然会多花点时间,但是有价值的
    2.测试用例设计过程中,包括作为试验执行这些测试用例时,常常可以在软件构建前
    就发现BUG。还有可能在测试设计阶段比测试执行阶段发现更多的BUG。
    3.执行用例的时候,正面测试用例测试出BUG,负面测试用例也会有相同的问题,但测试点并不是这个,可以考虑下轮测试执行,做到有效测试用例全覆盖又最有效率
    4.定义测试用例的执行顺序
    在测试用例执行过程中,你会发现每个测试用例都对测试环境有特殊的要求,或者对测试环境有特殊的影响。因此,定义测试用例的执行顺序,对测试的执行效率影响非常大。比如某些异常测试用例会导致服务器频繁重新启动,服务器的每次重新启动都会消耗大量的时间,导致这部分测试用例执行也消耗很多的时间。那么在编排测试用例执行顺序的时候,应该考虑把这部分测试用例放在最后执行,如果在测试进度很紧张的情况下,如果优先执行这部分消耗时间的异常测试用例,
    七.黑盒测试方法
    1.对等区间划分
    对等区间划分是测试用例设计的非常形式化的方法。它将被测软件的输入输出划分成一
    些区间,被测软件对一个特定区间的任何值都是等价的。
    2.边界值分析
    边界值分析使用对等区间划分相同的分析。但是,边界值分析假定错误最有可能出现在
    区间之间的边界。边界值分析将一定程度的负面测试加入到测试设计中,期望错误会在区间
    边界发生,对边界值的两边都需设计测试用例。
    3.错误猜测
    错误猜测大多基于经验,需要从边界值分析等其他技术获得帮助。这种技术猜测特定软件类型可能发生的错误类型,并且设计测试用例查出这些错误。
    八.搭建测试环境以及用例执行过程中遇到的问题
    1.搭建测试环境
    测试用例执行过程中,搭建测试环境是第一步。一般来说,软件产品提交测试后,开发人员应该提交一份产品安装指导书,在指导书中详细指明软件产品运行的软硬件环境,如果开发人员拒绝提供相关的安装指导书,搭建测试中遇到问题的时候,测试人员可以要求开发人员协助,这时候,一定要把开发人员解决问题的方法记录下来,避免同样的问题再次请教开发人员,这样会招致开发人员的反感,也降低了开发人员对测试人员的认可程度。 可以形成自己的安装说明文档。
    2.可测试性
    如果软件产品提供了日志功能,比如有软件运行日志、用户操作日志,一定在每个测试用例执行后记录相关的日志文件,作为测试过程记录,一旦日后发现问题,开发人员可以通过这些测试记录方便的定位问题。而不用测试人员重新搭建测试环境,为开发人员重现问题。
    3。发现bug时
    及时确认发现的问题: 测试执行过程中,如果确认发现了软件的缺陷,那么可以毫不犹豫的提交问题报告单。如果发现了可疑问题,又无法定位是否为软件缺陷,那么一定要保留现场,然后知会相关开发人员到现场定位问题。如果开发人员在短时间内可以确认是否为软件缺陷,测试人员给予配合;如果开发人员定位问题需要花费很长的时间,测试人员千万不要因此耽误自己宝贵的测试执行时间,可以让开发人员记录重新问题的测试环境配置,然后,回到自己的开发环境上重现问题,继续定位问题。
    4。及时更新测试用例
    测试执行过程中,应该注意及时更新测试用例。往往在测试执行过程中,才发现遗漏了一些测试用例,这时候应该及时的补充;往往也会发现有些测试用例在具体的执行过程中根本无法操作,这时候应该删除这部分用例;也会发现若干个冗余的测试用例完全可以由某一个测试用例替代,那么删除冗余的测试用例。 总之,测试执行的过程中及时地更新测试用例是很好的习惯。不要打算在测试执行结束后,统一更新测试用例,如果这样,往往会遗漏很多本应该更新的测试用例。
    5.提交一份优秀的问题报告单
    软件测试提交的问题报告单和测试日报一样,都是软件测试人员的工作输出,是测试人员绩效的集中体现。因此,提交一份优秀的问题报告单是很重要的。软件测试报告单最关键的域就是 “ 问题描述 ” ,这是开发人员重现问题,定位问题的依据。问题描述应该包括以下几部分内容:软件配置、硬件配置、测试用例输入、操作步骤、输出、当时输出设备的相关输出信息和相关的日志等。

    九.实际工作测试用例设计
    1.KC客户端
    主模块分解:登陆,注册,通讯录,IM,邮件,个人设置等等
    单个大模块再次分解:登陆流程(客户端,CS,注册系统,短信,权限,voip),每个系统模块完成它自己的任务,但是测试刚入门,怎么实现以及怎么测,怎么查看数据在后来比较长的时间内才越来越清晰,测试用例也越来越有效。这是测试范围
    技术基础:windows软件基础(内存,cpu,数据保存等)
    2.注册系统
    主模块分解:有许多种注册方式,每种注册方式的流程都有不同的地方,依赖的其他的模块也不相同,命令字也不同,所以可以再分解,开发人员会输出设计流程图(从网站到注册系统,到第三方系统验证,短信系统,数据库,权限系统,返回结果)
    每个流程也会有很多步骤,每个步骤都会有各种不同的环境条件输入输出情况,每个步骤都有它的实现方式,日志记录,数据库修改,数据输出,在流程中有很多业余规则(每种注册方式分配号码规格会不同,多少天不能重复注册等等)和设计规则(同一IP注册有时间限制,手机验证码最多发多少次等等),这些都是功能点,每个功能点可以设计出几个测试用例(正面的,负面的,边界的),负面的用例基本上会用到错误码。2个功能点存在某种关联性,可以设计出组合操作的用例出来进行测试。
    因为时间关系,可以直接测试整体流程,无法测试到的情况,用CT(开发人员开发的一个测试接口工具)来测试单个的模块,或者客户端还没出来,可以先测试命令字
    测试计划->模块分解->根据需求和设计文档设计测试用例->执行测试(需要观测的东西很多,可以在用例中写出数据库脚本,观察点)->发现BUG,并确定是BUG->补充测试用例->第二次版本测试->发现BUG,并确定是BUG->回归测试->验证测试
    版本修改的测试:数据库表修改->考虑影响到的范围有多少,然后测试多少,数据库表结构修改脚本也要测试,一方面了解脚本是否有问题,一方面了解上线时会花费多少时间。
                    业务流程修改->连模块都修改了,测试用例可以再利用,修改相应的流程,观察的点
                    某些规则修改->将旧的相关用例挖掘出来,更新并执行测试,并考虑到相关的功能点
                    最后一步,还是验证测试,跑完基本测试用例
    版本功能的增加:增加新功能点的测试用例并执行,是否影响到其他功能?需要测试
    3.CS系统
    当时测的是更新版本,添加和修改了一些功能
    同上,修改了哪些新增了哪些,功能点设计出用例,执行并观测过程和数据
    4.网站系统
    有4个工程:mykc,client,reg,card
    有2个web服务器:apache,tomcat
    环境:1个客户端(IE6.0),1个网络(内网),1个测试服务器(22)
    每个工程完成它自己的需求功能
    网站可分为静态和动态页面,静态页面有apache服务,目录为keepc。动态页面有tomcat服务,目录为4个工程。动态页面中的图片为keepc目录里的,同样需要这个目录。

    5.广告系统,邮件奖励系统

Open Toolbar