iOS测试驱动开发TDD理论与实战

2021-08-12  本文已影响0人  babyloveblues

前言

TDD技术主要分为两个模块需求拆分技术TDD流程
需求分析技术是将我们的一个完整的需求拆解成功能单元的技术
TDD流程解决的问题是如何使用TDD流程完成功能单元的代码编写

问题描述

火星漫步者在某块区域中根据指令进行移动,然后采集相应位置的火星数据。

火星车收到的指令分为四类:

  1. 探索区域信息:告知火星车,整片区域的长度(X)和宽度(Y)有多大;
  2. 初始化信息:火星车的降落地点(x, y)和朝向(N, S, E, W)信息;
  3. 移动指令:火星车可以前进(M);
  4. 转向指令:火星车可以左转 90 度(L)或右转 90 度(R)。

由于地球和火星之间的距离很远,指令必须批量发送,火星车执行完整批指令之后,再回报自己所在的位置坐标和朝向。

一、需求分析技术

任务拆解

(1)每个单元,都需要明确输入和输出。如果你不能确定这些输入和输出,那么你现在要做的事情就是继续理解需求,或者是找产品经理同学讨论。
(2)进行实现的时候,要遵循从右往左的原则。直到需求完全得到实现。

二、TDD流程

TDD环

下面我们就将以【转向】这个函数为例进行示范。


转向输入输出分析
typedef enum DIREDRTION {
    DIREDRTION_N = 0, // 北
    DIREDRTION_E, // 东
    DIREDRTION_S, // 南
    DIREDRTION_W, // 西
    DIREDRTION_UNKNOW // 未知方向
} DIREDRTION;

- (DIREDRTION)turn:(NSString *)cmd curDirection:(DIREDRTION)curDirection{
        return DIREDRTION_UNKNOW;
}

2.1 红

新建一个测试用例,测试用例是我们已知的事实,是我们对函数行为的预期。抽象地说就是:我们明确地知道输入将得到一个怎样的输出。

// 转向函数
// 输入:当前方向(向南)、转向命令(右转)
// 输出:转向后方向(向西)
- (void)testTurnCurDirection{
    MarsRover *rover = [[MarsRover alloc] init];
    {
        DIREDRTION direction = [rover turn:@"R" curDirection:DIREDRTION_S];
        XCTAssertTrue(direction == DIREDRTION_W,"turn:curDirection函数验证失败");
    }
}

运行测试,测试不通过。红!

2.2 绿

现在我们将以最小的成本去满足这个这个case,这样可以避免我们的程序的过度设计

// 转向函数
// 输入:当前方向(向南)、转向命令(右转)
// 输出:转向后方向(向西)
- (void)testTurnCurDirection{
    MarsRover *rover = [[MarsRover alloc] init];
    {
        DIREDRTION direction = [rover turn:@"R" curDirection:DIREDRTION_S];
        XCTAssertTrue(direction == DIREDRTION_W,"turn:curDirection函数验证失败");
    }
}

运行测试,通过。绿!

2.3重构

在写代码的过程中我们往往会闻到一些代码的坏味道,如果闻到了,请立即重构。暂时没闻到,咱们接着进行...

2.4 重复上述过程

我们不停的增加测试用例,并且不断的以最小代价完成代码功能以通过测试。

-(DIREDRTION)turn:(NSString *)cmd curDirection:(DIREDRTION)curDirection{
    cmd = [cmd uppercaseString];
    if (!([cmd isEqualToString:@"L"] || [cmd isEqualToString:@"R"])) {
        return  -1;
    }
    if ([cmd isEqualToString:@"L"]) {
        if (curDirection == DIREDRTION_S) {
            return DIREDRTION_E;
        }else if (curDirection == DIREDRTION_N) {
            return DIREDRTION_W;
        }else if (curDirection == DIREDRTION_E) {
            return DIREDRTION_N;
        }else{
            return DIREDRTION_S;
        }
    }
    if ([cmd isEqualToString:@"R"]) {
        if (curDirection == DIREDRTION_S) {
            return DIREDRTION_W;
        }else if (curDirection == DIREDRTION_N) {
            return DIREDRTION_E;
        }else if (curDirection == DIREDRTION_E) {
            return DIREDRTION_S;
        }else{
            return DIREDRTION_N;
        }
    }

    return  -1;
}

- (void)testTurn{
    MarsRover *rover = [[MarsRover alloc] init];
    {
        DIREDRTION direction = [rover turn:@"L" curDirection:DIREDRTION_E];
        XCTAssertTrue(direction == DIREDRTION_N,"turn:curDirection函数验证失败");
    }
    {
        DIREDRTION direction = [rover turn:@"L" curDirection:DIREDRTION_S];
        XCTAssertTrue(direction == DIREDRTION_E,"turn:curDirection函数验证失败");
    }
    
    {
        DIREDRTION direction = [rover turn:@"L" curDirection:DIREDRTION_W];
        XCTAssertTrue(direction == DIREDRTION_S,"turn:curDirection函数验证失败");
    }
}

[ 提醒 ] 细心的同学可能已经注意到我们把每个case都用一个大括号包装了起来,这样做的好处是,我们可以直接拷贝粘贴,而不用把精力放在维护变量名上。

2.5 重构

目前为止,我们已经完成了“转向”函数的编写,但是我们似乎可以闻到一些代码的坏味道,无论是测试代码还是被测代码。事实上他们都是需要被重构的,此处我们拿被测代码作为演示,并且体会单元测试是如何帮助我们进行代码重构的

DIREDRTION_N = 0, // 北
DIREDRTION_E = 1, // 东
DIREDRTION_S = 2, // 南
DIREDRTION_W = 3 // 西

经过观察,我们很容容易发现如下规律:

左转:最终方向 = (当前方向 + 3) % 4
右转:最终方向 = (当前方向 + 5) % 4

现在我们可以大刀阔斧的重构函数,因为如果我们的修改是不正确的,单元测试的case必然不会通过。重构之后的函数如下所示:

- (DIREDRTION)turn:(NSString *)cmd curDirection:(DIREDRTION)curDirection{
    cmd = [cmd uppercaseString];
    if (!([cmd isEqualToString:@"L"] || [cmd isEqualToString:@"R"])) {
        return  DIREDRTION_UNKNOW;
    }
    if ([cmd isEqualToString:@"L"]) {
        DIREDRTION direction = (curDirection + 3) % 4;
        return direction;
    }
    if ([cmd isEqualToString:@"R"]) {
        DIREDRTION direction = (curDirection + 5) % 4;
        return direction;
    }
    return  DIREDRTION_UNKNOW;
}

运行单元测试,测试通过。

事实上不仅仅是业务代码需要重构,我们的测试代码也需要重新构

三、再谈TDD的好处

1、不用“思考”

在使用TDD的开发方法进行开发的过程中,很多函数是随着测试用例的增长自然而然的就是浮现出来,并没有过多的去”思考“,尽管如此我们依仍认为这个过程是充满智慧的。

2、避免过度设计

在工作中,有一些代码,我们很容易就能感受到在代码设计上花费了很多心思,架构图非常复杂。我本人认为其实这是特别不好的一种现象,也许他能正常工作,但是对协同开发的工程师非常不友好。

3、交付代码的时候信心比较足

(1)因为有了测试代码的保障,我们就打好了整个测试的根基,尽管我们并不能保障产品的质量,但是我们可以很有信息的说,在代码层面,我们保持了一个较高的水准。

(2)没有用的代码坚决不写。

4、重构的保障

在一般的开发方法中,特别是发版之前,很多同学是不敢修改代码的,因为修改代码之后,很容易引起连锁反应,出现一系列预期之外的结果。但是如果我们有单元测试的保障,我们就可以非常放心的去重构我们的代码,只要我们通过了测试(当然我们也要保障case的质量)我们就可以相信,我们的代码依然处在较高的质量水平上。

五、参考

源码:UnitTestIniOS

上一篇 下一篇

猜你喜欢

热点阅读