TDD

Test Drive Development

  • 有利于更加专注软件设计;
  • 清晰地了解软件的需求;
  • 很好的诠释了代码即文档。

TDD

TDD是一种相对于普通思维的方式来说,比较极端的一种做法。我们一般能想到的是先编写业务代码,然后为其编写测试代码,用来验证产品方法是不是按照设计工作。而TDD的思想正好与之相反,在TDD的世界中,我们应该首先根据需求或者接口情况编写测试,然后再根据测试来编写业务代码,而这其实是违反传统软件开发中的先验认知的.

我们可以举一个生活中类似的例子来说明TDD的必要性:有经验的砌砖师傅总是会先拉一条垂线,然后沿着线砌砖,因为有直线的保证,因此可以做到笔直整齐;而新入行的师傅往往二话不说直接开工,然后在一阶段完成后再用直尺垂线之类的工具进行测量和修补。

TDD的好处不言自明,因为总是先测试,再编码,所以至少你的所有代码的public部分都应该含有必要的测试。另外,因为测试代码实际是要使用产品代码的,因此在编写产品代码前你将有一次深入思考和实践如何使用这些代码的机会,这对提高设计和可扩展性有很好的帮助,试想一下你测试都很难写的接口,别人(或者自己)用起来得多纠结。在测试的准绳下,你可以有目的有方向地编码;另外,因为有测试的保护,你可以放心对原有代码进行重构,而不必担心破坏逻辑。这些其实都指向了一个最终的目的:让我们快乐安心高效地工作。

BDD

Behavior Drive Development

DBB

XCTest(作者注:苹果官方测试框架)是基于OCUnit的传统测试框架,在书写性和可读性上都不太好。在测试用例太多的时候,由于各个测试方法是割裂的,想在某个很长的测试文件中找到特定的某个测试并搞明白这个测试是在做什么并不是很容易的事情。所有的测试都是由断言完成的,而很多时候断言的意义并不是特别的明确,对于项目交付或者新的开发人员加入时,往往要花上很大成本来进行理解或者转换。另外,每一个测试的描述都被写在断言之后,夹杂在代码之中,难以寻找。使用XCTest测试另外一个问题是难以进行mock或者stub,而这在测试中是非常重要的一部分。

行为驱动开发(BDD)正是为了解决上述问题而生的,作为第二代敏捷方法,BDD提倡的是通过将测试语句转换为类似自然语言的描述,开发人员可以使用更符合大众语言的习惯来书写测试,这样不论在项目交接/交付,或者之后自己修改时,都可以顺利很多

如果说作为开发者的我们日常工作是写代码,那么BDD其实就是在讲故事。一个典型的BDD的测试用例包活完整的三段式上下文,测试大多可以翻译为Given..When..Then的格式,读起来轻松惬意。BDD在其他语言中也已经有一些框架,包括最早的Java的JBehave和赫赫有名的Ruby的RSpec和Cucumber。而在objc社区中BDD框架也正在欣欣向荣地发展,得益于objc的语法本来就非常接近自然语言,再加上C语言宏的威力,我们是有可能写出漂亮优美的测试的。在objc中,现在比较流行的BDD框架有cedar,specta和Kiwi。

Quick + Nimble In Swift

https://github.com/Quick/Quick

// Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Quick
	import Nimble

	class TableOfContentsSpec: QuickSpec {
  	override func spec() {
    	describe("the 'Documentation' directory") {
      it("has everything you need to get started") {
        let sections = Directory("Documentation").sections
        expect(sections).to(contain("Organized Tests with Quick 	Examples and Example Groups"))
        expect(sections).to(contain("Installing Quick"))
      }

      context("if it doesn't have what you're looking for") {
        it("needs to be updated") {
          let you = You(awesome: true)
          expect{you.submittedAnIssue}.toEventually(beTruthy())
        }
      	}
    }
  	}
	}

Nimble

https://github.com/Quick/Nimble

1
2
3
4
5
6
7
!Swift
	expect(1 + 1).to(equal(2))
	expect(1.2).to(beCloseTo(1.1, within: 0.1))
	expect(3) > 2
	expect("seahorse").to(contain("sea"))
	expect(["Atlantic", "Pacific"]).toNot(contain("Mississippi"))
	expect(ocean.isClean).toEventually(beTruthy())

Specta

https://github.com/specta/specta

A light-weight TDD / BDD framework for Objective-C.

FEATURES

  • An Objective-C RSpec-like BDD DSL
  • Quick and easy set up
  • Built on top of XCTest
  • Excellent Xcode integration

Specta BDD DSL

  1. SpecBegin 声明了一个测试类,SpecEnd 结束了类声明
  2. describe (context) 块声明了一组实例
  3. it (example/specify) 是一个单一的例子
  4. beforeAll 是一个运行于所有同级块之前的块,只运行一次。afterAll 与beforeAll相反,是在所有同级块之后运行的块,只运行一次。
  5. beforeEach/afterEach,在每个同级块运行的时候,都会运行一次,而beforeAll/afterAll只会运行一次
  6. it/waitUntil/done(),异步调用,注意完成异步操作之后,必须调用done()函数,如下:
1
2
3
4
5
6
7
!Objc
	     it(@"should do some stuff asynchronously", ^{
        waitUntil(^(DoneCallback done) {
        // Async example blocks need to invoke done()     callback.
        done();
        });
  	     });

Expecta

https://github.com/specta/expecta

A matcher framework for Objective-C and Cocoa.

1
2
3
4
5
6
7
8
9
10
11
!Objc
	waitUntil(^(DoneCallback done) {
            //query
            [manager getSedentaryDataModels:^(NSArray < DMSedentaryDataModel * > *modelList) {
                tempModel = modelList.firstObject;
                expect(modelList.count).to.equal(1);
                done();
            }];
        });
        expect(tempModel).notTo.beNil();
        expect(tempModel.prKey).notTo.equal(0);

OCMock

http://ocmock.org/reference/#creating-mock-objects

  • Creating mock objects
  • Stubbing methods
  • Verifying interactions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
!Objc
	      __block id mockConnection = nil;
    
        beforeAll(^{
        
        });
    
        afterAll(^{
        
        });
    
        beforeEach(^{
        mockConnection = OCMClassMock([TwitterConnection class]);
        });
    
        afterEach(^{
        [mockConnection stopMocking];
        });
    
        it(@"is should be success", ^{
        
        TwitterViewController *controller = [[TwitterViewController alloc] init];
        controller.connection = mockConnection;
        
        //模拟fetchTweets方法返回预设值
        Tweet *testTweet = [[Tweet alloc] init];
        testTweet.userName = @"齐滇大圣";
        Tweet *testTweet2 = [[Tweet alloc] init];
        testTweet2.userName = @"美猴王";
        NSArray *tweetArray = @[testTweet,testTweet2];
        OCMStub([mockConnection fetchTweets]).andReturn(tweetArray);
        
        //模拟出来一个view类
        id mockView = OCMClassMock([TweetView class]);
        controller.tweetView = mockView;
        
        //这里执行updateTweetView之后,[mockView addTweet:]加入了testTweet和testTweet2
        [controller updateTweetView];
        
        OCMVerify([mockView addTweet:testTweet]);
        OCMVerify([mockView addTweet:testTweet2]);
        OCMVerify([mockView addTweet:[OCMArg any]]);

Slather

https://github.com/SlatherOrg/slather

1
2
3
4
5
6
!Objc
	source ~/.bash_profile
	cd ${SRCROOT}
	rm -rf  ${SRCROOT}/slather-html
	slather coverage --html --output-directory  ${SRCROOT}/slather-html --scheme DeviceManager --workspace ${SRCROOT}/DeviceManager.xcworkspace ${SRCROOT}/DeviceManager.xcodeproj
	open 'slather-html/index.html'