单元测试
一、单元测试和TDD
- 编程时需理清思路,将编程需求等想好,再开始编。此部分可用伪代码实现。
- 用编程语言将伪代码翻译一下,就是产品代码了。伪代码是产品代码最好的注释。
- 写了产品代码,还要写测试代码来证明自己的代码没有问题。Java编程时对类实现的测试叫单元测试。
关于单元测试的练习
需求:在一个MyUtil类中解决一个百分制成绩转成“优、良、中、及格、不及格”五级制成绩的功能。
根据需求撰写伪代码:
百分制转五分制:
如果成绩小于60,转成“不及格”
如果成绩在60与70之间,转成“及格”
如果成绩在70与80之间,转成“中等”
如果成绩在80与90之间,转成“良好”
如果成绩在90与100之间,转成“优秀”
其他,转成“错误”
用Java程序语言翻译伪代码为产品代码:
package experiment2;
public class MyUtil {
public static String percentage2fivegrade(int grade){
//如果成绩小于0,转成“错误”
if (grade<0) return "错误";
//如果成绩小于60,转成“不及格”
if (grade<60) return "不及格";
//如果成绩在60到70之间,转成“及格”
else if (grade<70) return "及格";
//如果成绩在70到80之间,转成“中等”
else if (grade<80) return "中等";
//如果成绩在80到90之间,转成“良好”
else if (grade<90) return "良好";
//如果成绩在90到100之间,转成“优秀”
else if (grade<=100) return "优秀";
//其他,转成“错误”
else return "错误";
}
}
接下来就是撰写测试代码,判断自己的产品代码有没有出错了: 选中MyUtil,在左边出现的小灯泡下拉菜单中选择Create Test
:
接着在创建出的MyUtilTest撰写测试代码:
import junit.framework.TestCase;
import org.junit.Test;
public class MyUtilTest extends TestCase {
@Test
public void testNormal() {
assertEquals("不及格", MyUtil.percentage2fivegrade(55));
assertEquals("及格", MyUtil.percentage2fivegrade(65));
assertEquals("中等", MyUtil.percentage2fivegrade(75));
assertEquals("良好", MyUtil.percentage2fivegrade(85));
assertEquals("优秀", MyUtil.percentage2fivegrade(95));
}
@Test
public void testException(){
assertEquals("错误",MyUtil.percentage2fivegrade(105));
assertEquals("错误",MyUtil.percentage2fivegrade(-55));
}
@Test
public void testBoundary(){
assertEquals("不及格",MyUtil.percentage2fivegrade(0));
assertEquals("及格",MyUtil.percentage2fivegrade(60));
assertEquals("中等",MyUtil.percentage2fivegrade(70));
assertEquals("良好",MyUtil.percentage2fivegrade(80));
assertEquals("优秀",MyUtil.percentage2fivegrade(90));
assertEquals("优秀",MyUtil.percentage2fivegrade(100));
}
}
以上测试代码中的testNormal()方法用于测试一般情况,而testException()和testBoundary()则是对意外情况和边界进行测试。如果三种测试中的一种没有通过,IDEA会提示是哪一块出错,以及出了什么错误:
我们可以从图中看出是边缘测试没有通过,原本应该显示“优秀”的显示了“错误”。于是回去看MyUtil类:
发现是设置“优秀”的范围时没有包含100分(我刚刚改的),所以100分便被归到了“错误”中。修改以后再次运行MyUtilTest,可看到三个测试都通过:
由此可看出,除了对普通数据的测试之外,边界情况和非法情况的测试也十分重要。
以TDD的方式研究学习StringBuffer
在老师的博客《积极主动敲代码,使用JUnit学习Java》中,给出了一个用于学习StringBuffer几个方法的程序:
public class StringBufferDemo{
public static void main(String [] args){
StringBuffer buffer = new StringBuffer();
buffer.append('S');
buffer.append("tringBuffer");
System.out.println(buffer.charAt(1));
System.out.println(buffer.capacity();
System.out.println(buffer.indexOf("tring"));
System.out.println("buffer = " + buffer.toString());
}
}
了解这几个方法以后,选择了charAt(),length(),capacity()这三个方法,开始撰写产品代码:
public class StringBufferDemo {
StringBuffer buffer=new StringBuffer();
public Character charAt(int i){
return buffer.charAt(i);
}
public int length(){
return buffer.length();
}
public int capacity(){
return buffer.capacity();
}
}
接着是撰写测试代码,来测试这三个方法是否正确:
import junit.framework.TestCase;
import org.junit.Test;
public class StringBufferDemoTest extends TestCase {
StringBuffer a=new StringBuffer("yesterday");
StringBuffer b=new StringBuffer("tomorrowisnotnow");
StringBuffer c=new StringBuffer("liveherenowtodayisneccessary");
@Test
public void testcharAt(){
assertEquals('y',a.charAt(0));
assertEquals('r',b.charAt(5));
assertEquals('n',c.charAt(8));
}
@Test
public void testlength(){
assertEquals(9,a.length());
assertEquals(16,b.length());
assertEquals(28,c.length());
}
@Test
public void testcapacity(){
assertEquals(25,a.capacity());
assertEquals(32,b.capacity());
assertEquals(44,c.capacity());
}
}
在测试代码中,我设定了三个长度逐渐增加的字符串。运行后,显示测试成功。
以TDD的方式开发一个复数类Complex
要求:定义属性并生成getter,setter;定义构造函数;定义公有方法(加减乘除)。
测试代码:
import junit.framework.TestCase;
import org.junit.Test;
public class ComplexTest extends TestCase {
Complex a=new Complex(1,2);
Complex b=new Complex(-2,-1);
Complex c=new Complex(4,-2);
Complex d=new Complex(4,-2);
@Test
public void testequals(){
assertEquals(false,a.equals(b));
assertEquals(false,b.equals(c));
assertEquals(true,c.equals(d));
}
@Test
public void testAdd(){
assertEquals(new Complex(-1,1),a.ComplexAdd(b));
assertEquals(new Complex(5,0),a.ComplexAdd(c));
}
@Test
public void testSub(){
assertEquals(new Complex(3,3),a.ComplexSub(b));
assertEquals(new Complex(-3,4),a.ComplexSub(c));
}
@Test
public void testMulti(){
assertEquals(new Complex(0,-5),a.ComplexMulti(b));
assertEquals(new Complex(8,6),a.ComplexMulti(c));
}
@Test
public void testDiv(){
assertEquals(new Complex(0,0.5),a.ComplexDiv(c));
assertEquals(new Complex(-0.3,-0.4),b.ComplexDiv(c));
}
}
产品代码:
public class Complex {
// 定义属性并生成getter,setter
private double r;
private double i;
// 定义构造函数
public Complex(double r,double i){
this.r=r;
this.i=i;
}
public static double getRealPart(double r){
return r;
}
public static double getImagePart(double i){
return i;
}
//Override Object
public boolean equals(Object obj){
Complex complex=(Complex) obj;
if (complex.r!=r) {
return false;
}
if(complex.i!=i){
return false;
}
return true;
}
public String toString(){
String str=new String();
if (i==0) str=r+"";
else if(i<0) str=r + ""+i+"i";
else str=r+""+"+"+i+"i";
return str;
}
// 定义公有方法:加减乘除
Complex ComplexAdd(Complex a){
return new Complex(r+a.r,i+a.i);
}
Complex ComplexSub(Complex a){
return new Complex(r-a.r,i-a.i);
}
Complex ComplexMulti(Complex a){
return new Complex(r*a.r-i*a.i,r*a.i+i*a.r);
}
Complex ComplexDiv(Complex a){
return new Complex((r*a.r+i*a.i)/(a.r*a.r+a.i*a.i),(i*a.r-r*a.i)/(a.r*a.r+a.i*a.i));
}
}
测试成功截图:
二、面向对象三要素
面向对象的三要素是封装、继承与多态。为借助抽象思维用好三要素:SOLID原则给出了指导。
- SRP,单一职责原则;
- OCP,开放-封闭原则;
- LSP,替换原则;
- ISP,接口分离原则;
- DIP,依赖倒置原则。
练习,体会OCP原则和DIP原则的应用
老师给出的设计模式示例代码如下:
// Server Classes
abstract class Data {
abstract public void DisplayValue();
}
class Integer extends Data {
int value;
Integer() {
value=100;
}
public void DisplayValue(){
System.out.println (value);
}
}
// Pattern Classes
abstract class Factory {
abstract public Data CreateDataObject();
}
class IntFactory extends Factory {
public Data CreateDataObject(){
return new Integer();
}
}
//Client classes
class Document {
Data pd;
Document(Factory pf){
pd = pf.CreateDataObject();
}
public void DisplayData(){
pd.DisplayValue();
}
}
//Test class
public class MyDoc {
static Document d;
public static void main(String[] args) {
d = new Document(new IntFactory());
d.DisplayData();
}
}
用我的学号(28)模6取余,得到4,须根据以下结果进行代码扩充:让系统支持Float类,并在MyDoc类中添加测试代码表明添加正确。
abstract class Data {
abstract public void DisplayValue();
}
class Integer extends Data {
int value;
Integer() {
value=100;
}
public void DisplayValue(){
System.out.println (value);
}
}
class Float extends Data{
float value;
Float(){
value=20155328;
}
public void DisplayValue(){
System.out.println(value);
}
}
// Pattern Classes
abstract class Factory {
abstract public Data CreateDataObject();
}
class IntFactory extends Factory {
public Data CreateDataObject(){
return new Integer();
}
}
class FloatFactory extends Factory{
public Data CreateDataObject(){
return new Float();
}
}
测试代码如下:
//Test class
public class MyDoc {
static Document d;
public static void main(String[] args) {
d = new Document(new FloatFactory());
d.DisplayData();
}
}
使用StarUML进行建模,画类图
三、实验中遇到的问题及解决方式
- 问题1:对StringBuffer的capacity()方法理解不太清晰。
- 解决方法:参考StringBuffer的Capacity详解及实践,知StringBuffer的默认大小是16,即如果长度小于16则默认容量为16,当StringBuffer达到最大容量时,会将最大容量增加到原来容量的两倍再加2.
- 问题2:在测试Complex类时,出现了这样的red bar:
- 解决方法:由图可知是equals()方法的撰写出现了问题,于是回到Complex中重新撰写如下:
public boolean equals(Object obj){
Complex complex=(Complex) obj;
if (complex.r!=r) {
return false;
}
if(complex.i!=i){
return false;
}
return true;
}
然后再次测试,显示测试成功。
四、实验体会
相比起第一次实验,觉得这次实验让自己受益匪浅。在实验课前花了不少的时间跟着老师的step by step教程慢慢学习,体会到了单元测试的实用性。以前写代码就算会自己带一些测试之类的东西来看看是否正确,但一旦结果出错也需要耗费较长的时间去找出错误,而单元测试就可以通过red bar和错误提示很快找到错误源头,然后解决。希望自己以后可以更好更熟练的掌握TDD编程方法吧。