您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
为初学者写ORM,ORM的原理及测试案例
 
作者:李福涛 来源:博客园 发布于 2015-04-22
   次浏览      
 

一 、什么是ORM?

概念: 对象关系映射(Object Relational Mapping,简称ORM,或O/RM,或O/R mapping),是一种程序技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。

详细介绍: 让我们从O/R开始。字母O起源于"对象"(Object),而R则来自于"关系"(Relational)。几乎所有的程序里面,都存在对象和关系数据库。在业务逻辑层和用户界面层中,我们是面向对象的。当对象信息发生变化的时候,我们需要把对象的信息保存在关系数据库中。

当你开发一个应用程序的时候(不使用O/R Mapping),你可能会写不少数据访问层的代码,用来从数据库保存,删除,读取对象信息,等等。你在DAL中写了很多的方法来读取对象数据,改变状态对象等等任务。而这些代码写起来总是重复的。

ORM解决的主要问题是对象关系的映射。域模型和关系模型分别是建立在概念模型的基础上的。域模型是面向对象的,而关系模型是面向关系的。一般情况下,一个持久化类和一个表对应,类的每个实例对应表中的一条记录,类的每个属性对应表的每个字段。

ORM技术特点:

1.提高了开发效率。由于ORM可以自动对Entity对象与数据库中的Table进行字段与属性的映射,所以我们实际可能已经不需要一个专用的、庞大的数据访问层。

2.ORM提供了对数据库的映射,不用sql直接编码,能够像操作对象一样从数据库获取数据。

二、反射以及Attribute在ORM中的应用。

什么是反射?

简单点吧,反射就是在运行时动态获取对象信息的方法,比如运行时知道对象有哪些属性,方法,委托等等等等。

反射有什么用呢?

反射不但让你在运行是获取对象的信息,还提供运行时动态调用对象方法以及动态设置、获取属性等的能力。

反射在ORM中有什么用呢?

我 这里所讨论的ORM实现是通过自定义Attribute的方式进行映射规则的描述的。但是我们并不知道具体哪个对象需要对应哪个表,并且这些对象是独立于 我们的ORM框架的,所以我们只能通过自定义Attribute来定义映射规则,然后通过反射来动态获取这些映射规则。

(这里只简单说明下概念:具体如何实现过程请看第四项。)

三、创建一个数据库表和表对应的实体model。

传统的创建表和model实体的创建过程。

1.创建数据库表

create table TB_People

(

     Pl_ID Int identity(1,1) primary key ,       

 

    PL_Age Int,

 

    Pl_Sex Nvarchar(4),

 

     Pl_LoginName nvarchar(30),

 

     Pl_TrueName  nvarchar(30),

 

     PL_Pwd  nvarchar(60)

)

2.根据表结构一般我们会创建如下model实体

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

namespace FuzhuKeji

{

    public class M_People

    {

 

        string _Pl_ID;

 

        public string Pl_ID

        {

            get { return _Pl_ID; }

            set { _Pl_ID = value; }

        }

 

        int _PL_Age;

 

        public int PL_Age

        {

            get { return _PL_Age; }

            set { _PL_Age = value; }

        }

 

        string _Pl_Sex;

 

        public string Pl_Sex

        {

            get { return _Pl_Sex; }

            set { _Pl_Sex = value; }

        }

 

        string _Pl_LoginName;

 

        public string Pl_LoginName

        {

            get { return _Pl_LoginName; }

            set { _Pl_LoginName = value; }

        }

 

        string _Pl_TrueName;

 

        public string Pl_TrueName

        {

            get { return _Pl_TrueName; }

            set { _Pl_TrueName = value; }

        }

        string _PL_Pwd;

        public string PL_Pwd

        {

            get { return _PL_Pwd; }

            set { _PL_Pwd = value; }

        }

    }

}

现在看到了表结构 和model实体,那如何根据model实体映射出表的插入语句及结构呢?下面我们就介绍有model映射到数据库表。

四、实体model如何映射出数据库表。

上面简单介绍了反射以及Attribute在ORM中的应用,那如何通过这些进行映射出来的呢?

方法一:

看到 题纲三中的model实体了,下面我们就通过反射的方法来动态获取此映射规则:

/// 
        ///  测试映射
        /// </summary>
        /// <param name="sender"></param>
/// <param name="e"></param> private void button1_Click(object sender, EventArgs e) { M_People mp = new M_People(); mp.PL_Age = 26; mp.Pl_ID = "001"; mp.Pl_LoginName = "Test1"; mp.PL_Pwd = "123"; mp.Pl_Sex = "男"; mp.Pl_TrueName = "张三"; PropertyInfo[] infos = mp.GetType().GetProperties(); string Message_shuxing1 = ""; foreach (PropertyInfo info in infos) { //获取属性并打印 Message_shuxing1 = Message_shuxing1 + (info.Name + ":" + info.GetValue(mp, null)); } MessageBox.Show("这里看到可以获得属性名称和属性值 (是不是对ORM有点慢慢明白了):"+Message_shuxing1); // 上面info.GetValue(mp, null)获得属性的值。 //info.SetValue(mp, "XX", null); 赋值 }

测试效果图如下:

是不是有点思路了,知道如何搞了,呵呵。

看到红色部分了吗

有感觉了没有:是不是和数据库的名称一样,而且还获得了值。为什么会出现这种情况呢?

属性是来至那?--Model实体吧,属性的名称也是model实体属性名称吧。所以我们只要把属性的名称按某个规则定义就可以获得其对应的数据库字段名和类型。

方法二:

备注下:其实不只这种方法可以完成ORM的映射,而且还可以通过Attribute:

Attribute中文翻译虽然也号称“属性”,但是她和对象的属性(Property)其实是完全不同的两概念。她是在运行时对对象或者对象属性、方法、委托等等进行描述的类,用于在运行时描述你的代码或者在运行时影响你的程序的行为。

其 实我们在c#的编程中经常看到Attribute,只不过我们没有注意罢了。比如Main函数前的“[STAThread]”这个其实就是一个 Attribute。全程为[STAThreadAttribute]。另外指定类可序列化的[Serializable]等等。是不是都很熟悉啊?只不 过平时估计没有用到,所以没有注意罢了。

既然Attribute是类,那么她的定义方法和类就没有两样了,唯一的不同就是自定义Attribute类必须继承于System.Attribute。

那我们改下M_People实体的东西如下:

下面我们来简单定义一个描述数据库字段信息的Attribute,在此类中我们采用更省略的方式,仅仅提供“字段名”,“字段类型”:

public class DataFieldAttribute : Attribute

    {

        private string _FieldName;

        private string _FieldType;

        public DataFieldAttribute(string fieldname, string fieldtype)

        {

            this._FieldName = fieldname;

            this._FieldType = fieldtype;

        }

        public string FieldName

        {

            get { return this._FieldName; }

            set { this._FieldName = value; }

        }

        public string FieldType

        {

            get { return this._FieldType; }

            set { this._FieldType = value; }

        }

    }

那我们把Mode更改下改为如下:

public class M_People

    {

 

        string _Pl_ID;

        [DataFieldAttribute("Pl_ID", "Int")]
        public string Pl_ID

        {
            get { return _Pl_ID; }
            set { _Pl_ID = value; }
        }
        int _PL_Age;
        [DataFieldAttribute("PL_Age", "Int")]
        public int PL_Age

        {

            get { return _PL_Age; }
            set { _PL_Age = value; }

        }
        string _Pl_Sex;
        [DataFieldAttribute("Pl_Sex", "nvarchar")]
        public string Pl_Sex
        {
            get { return _Pl_Sex; }
            set { _Pl_Sex = value; }
        }
        string _Pl_LoginName;
        [DataFieldAttribute("Pl_LoginName", "nvarchar")]
        public string Pl_LoginName
        {
            get { return _Pl_LoginName; }

            set { _Pl_LoginName = value; }
        }

        string _Pl_TrueName;

        [DataFieldAttribute("Pl_TrueName", "nvarchar")]

        public string Pl_TrueName

        {

            get { return _Pl_TrueName; }

            set { _Pl_TrueName = value; }

        }

        string _PL_Pwd;

        [DataFieldAttribute("PL_Pwd", "nvarchar")]

        public string PL_Pwd

        {

            get { return _PL_Pwd; }

            set { _PL_Pwd = value; }

        }
    }

通过自定义Attribute,我们定义了类属性和数据库字段的一一对应关系。

那我们通过事件测试下方法案例:

/// 
        /// 反射+Attribute 映射出数据库表
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button2_Click(object sender, EventArgs e)
        {
            M_People mp = new M_People();
            mp.PL_Age = 26;
            mp.Pl_ID = "001";
            mp.Pl_LoginName = "Test1";
            mp.PL_Pwd = "123";
            mp.Pl_Sex = "男";
            mp.Pl_TrueName = "张三";

            PropertyInfo[] infos = mp.GetType().GetProperties();
            string Str_TestAtrrubute = "";

            object[] objDataFieldAttribute = null;
            foreach (PropertyInfo info in infos)
            {
                objDataFieldAttribute = info.GetCustomAttributes(typeof(DataFieldAttribute), false);
                if (objDataFieldAttribute != null)
                {
                  Str_TestAtrrubute=Str_TestAtrrubute+(info.Name + "->数据库字段:"
 + ((DataFieldAttribute)objDataFieldAttribute[0]).FieldName)+" --------";
                }
            }

            MessageBox.Show(Str_TestAtrrubute);
        }

测试的效果图如下:

哈哈,你是不是很想动手了啊?

加油!下面我们就介绍如何实现插入语句的映射!

五、组合ORM映射生成insert语句。

我们仔细思考下看到上面的我们会怎么想才可以生成一条sql语句并且执行。

首先我们是不是应该分开两部分

第一步负责生成插入语句。

第二步是负责执行插入语句得。

我们继续思考?生成一条插入语句我们要考虑哪些问题?

a、是不是返回值

b、是不是要判断表中是否有不需要组合为插入语句的字段(如自增字段)

c、而且这个插入语句是针对对象的插入语句而不是固定的某个或者已知的某个实体。所以我们会考虑到泛型的使用。

这样我们基本确定了insert语句的参数和结构。

我们再回到第一步如何根据实体生成插入语句? 我们第四部也只是说了根据实体映射出和数据库字段一样的名字,这有什么用呢?

肯定根据这些字段名我们要想办法组合个sql语句。继续分析、、、

如何分工 :肯定要给个执行的sql语句

分工一:是不是获得属性名称组合sql。

分工二:是不是做个参数的对应表。

分工三:组合这些东西,并把其属性类型和数据库字段类型对应起来。

上面说了那么多,只是帮大家打开思路,其实只要你理解了映射(第四项),用自己的思路去写那些组合sql也可以得,

我这个地方写的也不见得完美,只是给大家做个例子,嘿嘿,一起加油!

有几个地方用到了枚举首先我列出枚举的方法:

第一个属性标识是否为主键或者读写的标识

[Serializable]

  [Flags]

  public enum ColumnKeyType

  {

      /// 

      /// 默认状态

      /// 

      Default = 1,

 

      /// 

      /// 标识为主键

      /// 

      Identity = 2,
      /// 

      /// Extend状态下,不参与读取、增加、修改

      /// 

      Extend = 4,

 

      /// 

      /// Read状态下不参与增加、修改

      /// 

      Read = 8

  }

 

返回值做了枚举:

  public enum DBReturnType

  { /// 

      /// 返回受影响的行数

      /// 

      EffectRow,

      /// 

      /// 返回最后插入的主键值

      /// 

      Identity

  }

插入语句的代码:

#region 把对象内容保存到数据库中 Insert

      /// 

      /// 把对象内容保存到数据库中

      /// 

      /// 
      /// <param name="model"></param>

      /// &lt;param name=&quot;isIncludeKeyColumn&quot;&gt;插入语句中是否包含对主键的插入,
当主键值为自动增加时为false&lt;/param&gt;
/// <param name="returnType">返回的数据类型:DBReturnType.EffectRow 为返回受影响行数; DBReturnType.IdEntity 返回最新插入主键值(isIncludeKeyColumn == false时有效)</param> public static int Insert(T model, bool isIncludeKeyColumn, DBReturnType returnType) where T : class { int i = 0; Type type = typeof(T); //获取表名 string tableName = EntityHelper.GetTableName(type); PropertyInfo[] pis = type.GetProperties(); //获取所有字段和主键名称 List columns = null; //处理是否包含主键插入 if (isIncludeKeyColumn == false) { columns = EntityHelper.GetTableColumns(pis, ColumnKeyType.Identity | ColumnKeyType.Extend, null); } else { columns = EntityHelper.GetTableColumns(pis, ColumnKeyType.Extend, null); } //生成INSERT语句 StringBuilder sqlText = new StringBuilder(); sqlText.Append("INSERT INTO "); sqlText.Append(tableName); sqlText.Append(" ("); //第一个字段 sqlText.Append(columns[0]); //第二个起所有字段 int loop = columns.Count; for (i = 1; i < loop; i++) { sqlText.Append(","); sqlText.Append(columns[i]); } sqlText.Append(") VALUES ("); //第一个字段 sqlText.Append("@"); sqlText.Append(columns[0]); //第二个起所有字段 for (i = 1; i < loop; i++) { sqlText.Append(",@"); sqlText.Append(columns[i]); } sqlText.Append(");"); //生成MySqlParamter PropertyInfo propertyInfo = null; SqlParameter[] paras = new SqlParameter[loop]; for (i = 0; i < loop; i++) { propertyInfo = type.GetProperty(columns[i]); paras[i] = new SqlParameter(columns[i], GetMySqlDbType(propertyInfo.PropertyType), -1); paras[i].Value = propertyInfo.GetValue(model, null); } //根据两种情况返回不同的值 if (isIncludeKeyColumn == false && returnType == DBReturnType.Identity) { sqlText.Append(" SELECT @@identity AS RetId"); SqlDataReader sdr = DataReader(sqlText.ToString(), CommandType.Text, paras); int keyId = 0; if (sdr.Read()) { keyId = Convert.ToInt32(sdr["RetId"]); } sdr.Close(); return keyId; } else { return NonQuery(sqlText.ToString(), CommandType.Text, paras); } } #endregion #region 根据Type类型获取SQL的数据类型 /// /// 根据Type类型获取MySQL的数据类型 /// /// <param name="type"></param> /// private static SqlDbType GetMySqlDbType(Type type) { SqlDbType dbtype = SqlDbType.VarChar; if (type.Equals(typeof(string))) { } else if (type.Equals(typeof(int))) { dbtype = SqlDbType.Int; } else if (type.Equals(typeof(bool))) { dbtype = SqlDbType.Bit; } else if (type.Equals(typeof(DateTime))) { dbtype = SqlDbType.DateTime; } else if (type.Equals(typeof(decimal))) { dbtype = SqlDbType.Decimal; } else if (type.Equals(typeof(float))) { dbtype = SqlDbType.Float; } else if (type.Equals(typeof(double))) { dbtype = SqlDbType.Float; } return dbtype; } #endregion

下面我们简单定义一个描述数据库字段信息的Attribute 包括表名 属性字段获得

从Model模型中获取数据表名、主键名、 获取需要的读取数据源的字段集

(忘了说明表的表名写在那个地方,其实表名只需要定义在类的上面就可以了)为了简单起见我还是把目前的model放到代码里面

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

namespace FuzhuKeji

{

    [Serializable]

    [Property("TB_People")]

    public class M_People

    {

 

        string _Pl_ID;

 

        /// 

        /// 主键

        /// 

        [Property(ColumnKeyType.Identity)]

        public string Pl_ID

        {

            get { return _Pl_ID; }

            set { _Pl_ID = value; }

        }

 

        int _PL_Age;

        

        public int PL_Age

        {

            get { return _PL_Age; }

            set { _PL_Age = value; }

        }

 

        string _Pl_Sex;

     

        public string Pl_Sex

        {

            get { return _Pl_Sex; }

            set { _Pl_Sex = value; }

        }

 

        string _Pl_LoginName;

         

        public string Pl_LoginName

        {

            get { return _Pl_LoginName; }

            set { _Pl_LoginName = value; }

        }

 

        string _Pl_TrueName;

      

        public string Pl_TrueName

        {

            get { return _Pl_TrueName; }

            set { _Pl_TrueName = value; }

        }

 

        string _PL_Pwd;

    

        public string PL_Pwd

        {

            get { return _PL_Pwd; }

            set { _PL_Pwd = value; }

        }

 

    }

}

好吧这样就不用担心了映射表名获得字段的代码在下面:

#region 下面我们简单定义一个描述数据库字段信息的Attribute 包括表名 属性字段获得

  public class PropertyAttribute : Attribute

  {

      public string tableName;

      public ColumnKeyType columnKeyType;

 

      /// 

      /// 重构方法默认值

      /// 

      public PropertyAttribute()

      {

          this.columnKeyType = ColumnKeyType.Default;

      }

 

      /// 

      /// 

      /// 

      /// <param name="tableName"></param>

      public PropertyAttribute(string tableName)

      {

          this.tableName = tableName;

      }

 

      public PropertyAttribute(ColumnKeyType columnKeyType)

      {

          this.columnKeyType = columnKeyType;

      }

  }

 

  #endregion

 

  #region  从Model模型中获取数据表名、主键名、 获取需要的读取数据源的字段集

 

  public class EntityHelper

  {

      /// 

      /// 从Model模型中获取数据表名

      /// 

      public static string GetTableName(Type type)

      {

          PropertyAttribute property = (PropertyAttribute)(type.GetCustomAttributes(false)[0]);

          return property.tableName;

      }

 

 

 

      /// 

      /// 从Model模型中获取数据主键名

      /// 

      public static PropertyInfo GetTableIdentity(PropertyInfo[] pis)

      {

          object[] infos = null;

          PropertyAttribute attribute = null;

          foreach (PropertyInfo pi in pis)

          {

              infos = pi.GetCustomAttributes(false);

              if (infos.Length > 0)

              {

                  attribute = (PropertyAttribute)(infos[0]);

                  if (attribute.columnKeyType == ColumnKeyType.Identity)

                  {

                      return pi;

                  }

              }

          }

 

          return null;

      }

 

      /// 

      /// 获取需要的读取数据源的字段集

      /// 

     
 /// <param name="pis">Model模型所有属性集合</param>
      
/// <param name="filter"></param>
      /// <param name="customColumns">自定义查询列名集合,使用逗号分隔。如不需要则为null</param>

      /// 

      public static List GetTableColumns(PropertyInfo[] pis, ColumnKeyType filter, string customColumns)

      {

          string col = "";

          return GetTableColumns(pis, filter, customColumns, ref col);

      }
      /// 

      /// 获取需要的读取数据源的字段集

      /// 

      /// <param name="pis">Model模型所有属性集合</param>

      /// <param name="filter"></param>

      /// <param name="customColumns">自定义查询列名集合,使用逗号分隔。如不需要则为null</param>

      /// 

      public static List GetTableColumns(PropertyInfo[] pis, ColumnKeyType filter, string customColumns, ref string outCol)

      {

          List columns = new List();

          if (customColumns != null && customColumns.Length > 0)

          {

              /*

               * 需要安全处理

               * 限制字段不包含空格

               */

              customColumns = customColumns.Trim();

              string[] strs = customColumns.Split(',');

              foreach (string str in strs)

              {

                  if (IsRegexMatch(str, @"^(\w[^\s';]+)$"))

                  {

                      columns.Add(str);

                  }

              }

              outCol = customColumns;

          }

          else

          {

              object[] infos = null;

              PropertyAttribute attribute = null;

              foreach (PropertyInfo pi in pis)

              {

                  //删除外部扩展对象项

                  infos = pi.GetCustomAttributes(false);

                  if (infos.Length > 0)

                  {

                      attribute = (PropertyAttribute)(infos[0]);

                      if (attribute.columnKeyType == (filter & attribute.columnKeyType))

                      {

                          continue;

                      }

                  }

                  outCol += string.Concat(",", pi.Name);

                  columns.Add(pi.Name);

              }

              outCol = outCol.Remove(0, 1);

          }

          return columns;

      }
      /// 

      /// 检查是否满足某种正则表达式

      /// 

      private static bool IsRegexMatch(string str, string Express)

      {

          if (string.IsNullOrEmpty(str))

          {
              return false;
          }
          return Regex.IsMatch(str, Express);

 

      }

  }

 

  #endregion

上面就完成了sql语句的生成:下面我就定义一个sql语句的执行就可以了。其实你到insert方法里会发现 在生产完sql语句就调用执行方法了。

/// 

/// 配置字符串参数

/// 

private static void PrepareCommand(SqlConnection conn, SqlTransaction trans, 
SqlCommand sqlCommand, string sqlText, CommandType commandType, SqlParameter[] parms)

{

    if (conn.State != ConnectionState.Open)

    {

        conn.Open();

    }

 

    sqlCommand.Connection = conn;

    sqlCommand.CommandText = sqlText;

    sqlCommand.CommandType = commandType;

 

    if (trans != null)

    {

        sqlCommand.Transaction = trans;

    }

 

    if (parms != null)

    {

        foreach (SqlParameter parm in parms)

        {

            sqlCommand.Parameters.Add(parm);

        }

    }

}

 

 

/// 

/// 执行SQL语句,返回数据集

/// 

public static SqlDataReader DataReader(string sqlText, CommandType commandType, SqlParameter[] parms)

{

    SqlConnection conn = new SqlConnection(@"Data Source=HAOFUQI\SQLEXPRESS;
Initial Catalog=Fukusuke;Persist Security Info=True;User ID=sa;pwd=123");

    SqlCommand sqlCommand = new SqlCommand();

    PrepareCommand(conn, null, sqlCommand, sqlText, commandType, parms);

 

    SqlDataReader reader = sqlCommand.ExecuteReader(CommandBehavior.CloseConnection);

 

    sqlCommand.Dispose();

    return reader;

}

 

 

/// 

/// 执行SQL语句,并返回影响行数

/// 

public static int NonQuery(string sqlText, CommandType commandType, SqlParameter[] parms)

{

    int reVal = 0;

    using (SqlConnection conn = new SqlConnection(@"Data Source=HAOFUQI\SQLEXPRESS;
Initial Catalog=Fukusuke;Persist Security Info=True;User ID=sa;pwd=123"))

    {

        SqlCommand sqlCommand = new SqlCommand();

        PrepareCommand(conn, null, sqlCommand, sqlText, commandType, parms);

 

        reVal = sqlCommand.ExecuteNonQuery();

      

        sqlCommand.Parameters.Clear();

        sqlCommand.Dispose();

    }

 

    return reVal;

六、测试ORM的插入映射。

 /// 
        ///  执行插入语句
        /// </summary>
/// <param name="sender"></param>
/// <param name="e"></param> private void button3_Click(object sender, EventArgs e) { M_People mp = new M_People(); mp.PL_Age = 26; mp.Pl_ID = "001"; mp.Pl_LoginName = "Test1"; mp.PL_Pwd = "123"; mp.Pl_Sex = "男"; mp.Pl_TrueName = "张三"; int Insert_Key= DBHelper.Insert(mp,false ,DBReturnType.Identity); MessageBox.Show("添加成功! 插入数据的主键为:"+Insert_Key.ToString()); }

测试结果如下:

数据库插入效果:

这个地方就插入语句成功了。

七、总结。

这篇文章写得不是很好,我只是想描述,却没有一层层去剥开,也许这篇文章太长了吧。
这篇文章的核心应该是说明白如何映射如何到sql。没有分层的概念。看起来混乱,但是如果分层我就要更多篇幅讲解。


写的错误或者不好的地方还请多多批评指导。

上一篇写的是:为初学者写三层.为初学者写三层,三层的搭建和测试例子

下面准备写ORM框架,然后把生成器集成到框架中。(伪三层+ORM)+生成器+常用类库 用一个财务报销系统作为案例。

好了就写到这吧!

   
次浏览       
相关文章

微服务测试之单元测试
一篇图文带你了解白盒测试用例设计方法
全面的质量保障体系之回归测试策略
人工智能自动化测试探索
相关文档

自动化接口测试实践之路
jenkins持续集成测试
性能测试诊断分析与优化
性能测试实例
相关课程

持续集成测试最佳实践
自动化测试体系建设与最佳实践
测试架构的构建与应用实践
DevOps时代的测试技术与最佳实践
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...