本文通过TDD设计模式实现一个简易的struts框架。
一. 什么是TDD设计模式?
TDD是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD虽是敏捷方法的核心实践,但不只适用于XP(Extreme Programming),同样可以适用于其他开发方法和过程。
TDD的基本思路就是通过测试来推动整个开发的进行,它以不断的测试推动代码的开发,既简化了代码,又保证了软件质量,测试驱动开发技术并不只是单纯的测试工作。
二.设计需求
本文设计了一个lite-struts,主要功能为在网页登陆界面(login),输入用户名(name)及密码(password),如果与后台预设的用户名(name)和密码(password)一致,则返回成功的信息(success),并打开相应的页面(成功情况下的jsp);如果失败,则返回失败的信息(fail),并打开相应的页面(失败情况下的jsp)。在网页登出界面(logout),与登陆界面思想一样,分为成功(success)和错误(error)两种响应,并打开相对应的界面。
具体的功能需求如下struts.xml文件所示:
1 | <?xml version="1.0" encoding="UTF-8"?> |
三.设计思路
采用先写测试用例再写主要功能代码的TDD模式进行驱动设计,本文主要分析登陆的代码设计,登出的思想和登陆一样,就不赘述。
整体设计思路如下:
- 第一步:解析xml。
- 第二步:根据action的name确定对应的class。
- 第三步:通过反射获取所有的set方法,并调用set方法将相应的name和password存入到Map中(key-value对,name=?,password=?,即登陆时输入的name和password)。
- 第四步:调用方法(本文中为exeute方法)确定name和password是否正确,正确返回信息success,错误则返回信息fail。
- 第五步:通过反射获取所有的get方法,反射调用并把值和属性形成一个HashMap,例如:{“name”,”test”},{“password”,”123456”}{“message”,”login successful.”}(如果name和password均正确,则会得到message为login successful.),并存入到视图文件(本文为类View)的Map数据段中(本文为parameters)。
- 第六步:通过action的name以及第四步返回的提示信息(success/fail)进行判断,确定相对应的jsp页面,并存储到视图文件的jsp中。
- 第七步:返回View。即通过传入的actionName以及存有name和password的值的parameters(Map类型),从第一步到第六步确定了相应的视图View,最后返回得到相应的试图View,视图View中主要存有jsp和相应的message提示(本文用到这两项进行junit测试)。
1.第一个测试用例:统揽全局,上帝视角分析核心需求。
当我们进入登陆界面时,传入的是login,此时需要输入name和password以验证登陆,如果均正确,则打开成功的jsp页面,并提示成功;如果验证失败,则提示失败并留在登陆界面以继续输入name和password。核心测试代码如下,该测试若能通过,则我们的任务就全部完成了。
1 | package com.coderising.litestruts; |
此时类Struts和类View以及这两个类下有一些方法出现了红线提示错误,这时我们采用IDE自动补全代码的方式创建这两个类以及相应的方法。
先写核心类struts.java,我们所有的各模块核心功能代码都会在这个类中组装以完成我们最终的lite-struts。类struts骨架代码如下:
1 | package com.coderising.litestruts; |
核心骨架代码很简单,runAction方法中第一个参数为传入一个actionName字符串以判断action是login还是logout,我们这主要测试login,logout类似处理。第二个参数采用Map传入name和password以验证正确性,最后返回对应的View视图。这个骨架一目了然,我们先把它放在这里不用管,后面通过一步步TDD设计逼近最终功能,最后再来补全核心代码。
我们还需要一个视图文件,以返回正确的对应视图。相关代码如下:
1 | package com.coderising.litestruts; |
我们这里还需要一个登陆文件以用来设置和获取name和password,以及验证name和password正确性的方法(在此登陆文件中设置好了正确的name和password),以用来提供success或fail的提示信息。相关代码如下:
1 | package com.coderising.litestruts; |
2.第二个测试用例:解析xml,获取xml到java的反射。
首先,我们需要解析xml文件,并通过传入的action的name是login还是logout来获取相应的class文件。我们的思想是先把xml文件传入一个配置文件(此处用的Configuration.java)中,并将其解析,然后通过传入”login”或”logout”参数得到与其相对应的class名。测试代码如下所示:
1 | @Test |
此时Configuration类和getClassName方法下会出现红线提示错误,这时只需根据下面的提示新建Configuration类和相应的getClassName方法就可以了,建好之后红线就会消失。然后就把新建好的Configuration类的构造函数和getClassName方法写好即可。
然后,我们需要根据登陆(login)或登出(logout)以及其操作后得到的提示信息(成功或失败/错误)来确定需要获取哪个jsp页面,具体的测试代码如下:
1 | @Test |
同样,我们把红线提示错误的getResultView方法加入到Configuration类中,并把该获取相应jsp的功能代码写好。
此时,第二个测试用例完成了,并在相对应的功能代码中完成了相关设计。
第二个测试用例完整代码如下:
1 | package com.coderising.litestruts; |
第二个测试用例相对应的解析xml,获取相对应的class以及jsp的完整功能代码如下:
1 | package com.coderising.litestruts; |
此处采用了jdom解析xml文件,需要导入相对应的jar包。
相对应的异常处理代码:
1 | package com.coderising.litestruts; |
3.第三个测试用例:通过反射操作对象。
首先,我们需要通过反射获得所有的set方法,以用来设置name和password(即登陆时输入的name和password),测试代码如下:
1 | @Test |
根据提示创建RefletionUtil类和相应的getSetterMethods方法,完成相应的代码设计。
反射得到了所有的set方法之后,我们需要调用set方法来设置name和password,并把name=?,password=?这样的key-value对存储到Map中。相关测试代码如下:
1 | @Test |
根据提示创建RefletionUti类和相应的setParameters方法,完成相应的代码设计。
反射获取所有的get方法,相关测试代码如下:
1 | @Test |
根据提示创建RefletionUti类和相应的getGetterMethods方法,完成相应的代码设计。
通过反射获取所有的get方法后,反射调用get方法以获取一一对应的name和password以及message的值,相关测试代码如下:
1 | @Test |
根据提示创建RefletionUti类和相应的getParamterMap方法,完成相应的代码设计。
第三个测试用例完整代码如下:
1 | package com.coderising.litestruts; |
第三个测试用例对应的功能代码如下:
1 | package com.coderising.litestruts; |
此处采用了代码重构,getGetterMethods方法和getSetterMethods方法除了”get”和”set”这一点区别外完全一致(如上述代码Backup分界符下面的代码所示,方法名后面加了_V1以防止重名错误),所以采用重构把不同之处(字符串”get”和”set”)以第二个方法参数传入新的getMethods方法中,原来的getGetterMethods方法和getSetterMethods方法只需直接return并新传入字符串方法参数”get”或”set”即可,这样代码变得会更加简洁。
最后,我们只需把最开始的核心骨架代码struts.java补全即可,代码如下所示:
1 | package com.coderising.litestruts; |
此时,运行最开始的StrutsTest.java测试文件,通过即完成所有的测试。
如上图,StrutsTest测试通过,即我们的简易struts就大功告成了。
特别注意:在上述各个模块的单元测试中,每一个方法测试(@Test)在写完相对应的一个方法后,例如:第二个测试用例的第一个测试testGetClassName(),测试通过action的name得到相对应的class,在对应的类Configuration中写完getClassName()方法后,即运行测试已验证该方法是否书写正确。其他的设计都是一样,写一个@Test,写一个方法,运行测试进行验证该方法的正确性,以此来逼近最终要实现的功能。本文直接给出了最后的三个测试用例运行成功的图,但是按一个@Test写一个对应的方法就进行测试验证一步一步走到最后的,需谨记,这才是TDD设计模式的核心思想。