20140304 i os-behavior-driven-development-jp

67 %
33 %
Information about 20140304 i os-behavior-driven-development-jp

Published on March 9, 2014

Author: bgesiak

Source: slideshare.net

iOS ビヘイビア駆動開発 KiwiとNocillaでRESTfulなアプリのテスト 2014年3月9日 Brian Gesiak 研究生、東京大学 @modocache #startup_ios

内容 • ビヘイビア駆動開発(BDD)とは • iOSでBDDを実践する例 • Kiwi • 非同期通信(HTTP)のテスト方法 • Nocilla

通信のBDD サンプルアプリ • あるGitHubユーザのレポジトリを 表示するアプリを作りたい • GitHub APIからJSONをGETできる • https://api.github.com/users/ {{ username }}/repos.json

通信のBDD サンプルアプリ /// GET /users/:username/repos ! [ { "id": 1296269, "name": "Hello-World", "description": "My first repo!", /* ... */ } ]

BDDで作ってみましょう Kiwiを使ってBDD実践 • ビヘイビア駆動開発とはテスト駆動開発から派生し た概念

テスト駆動開発とは Red-Green-Refactorサイクル • Red: ! • Green: ! • Refactor:

テスト駆動開発とは Red-Green-Refactorサイクル • Red: 失敗するテストを書く ! • Green: ! • Refactor:

テスト駆動開発とは Red-Green-Refactorサイクル • Red: 失敗するテストを書く ! • Green: テストが成功するようにコードを書く ! • Refactor:

テスト駆動開発とは Red-Green-Refactorサイクル • Red: 失敗するテストを書く ! • Green: テストが成功するようにコードを書く ! • Refactor: 重複をなくす

テスト駆動開発とは Red-Green-Refactorサイクル • Red: 失敗するテストを書く ! • Green: テストが成功するようにコードを書く ! • Refactor: 重複をなくす 繰り返す

XCTestを使ったiOS TDDの一例

XCTestを使ったiOS TDDの一例 // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end

XCTestを使ったiOS TDDの一例 // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end

XCTestを使ったiOS TDDの一例 // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end

XCTestを使ったiOS TDDの一例 // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end

XCTestを使ったiOS TDDの一例 // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end

XCTestを使ったiOS TDDの一例 // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end

XCTestを使ったiOS TDDの一例 あえて言おう! カスであると!

ビヘイビア駆動開発(BDD) • 「何をテストすればいいのか」 • コードをテストするのではなく、求めている挙動を 説明(specify)する

KiwIを使ったiOS BDDの一例

KiwIを使ったiOS BDDの一例 // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });

KiwIを使ったiOS BDDの一例 // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });

KiwIを使ったiOS BDDの一例 // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });

KiwIを使ったiOS BDDの一例 // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });

KiwIを使ったiOS BDDの一例 // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });

KiwIを使ったiOS BDDの一例 // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });

Kiwiのメリット

Kiwiのメリット • Setup、teardownを無限にネストできる

Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); });

Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ • Mock、stubも入っている /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); });

Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mock、stubも入っている [collection stub:@selector(addRepo:)];

Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mock、stubも入っている [collection stub:@selector(addRepo:)]; • 非同期テストをサポート

Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mock、stubも入っている [collection stub:@selector(addRepo:)]; • 非同期テストをサポート [[collection.repos shouldEventually] haveCountOf:2];

Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mock、stubも入っている [collection stub:@selector(addRepo:)]; • 非同期テストをサポート [[collection.repos shouldEventually] haveCountOf:2]; • XCTestより読みやすい

まずは失敗するテストを

まずは失敗するテストを /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1]; });

まずは失敗するテストを /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1]; });

まずは失敗するテストを /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1]; });

まずは失敗するテストを /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1]; });

まずは失敗するテストを /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1]; });

まずは失敗するテストを /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1]; });

テストを成功させる

テストを成功させる /// GHVAPIClient.m ! // Create a request operation manager pointing at the GitHub API NSString *urlString = @"https://api.github.com/"; NSURL *baseURL = [NSURL URLWithString:urlString]; AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL]; ! // The manager should serialize the response as JSON manager.requestSerializer = [AFJSONRequestSerializer serializer]; ! // Send a request to GET /users/:username/repos [manager GET:[NSString stringWithFormat:@"users/%@/repos", username] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // Send response object to success block success(responseObject); } failure:nil]; }

テストを成功させる /// GHVAPIClient.m ! // Create a request operation manager pointing at the GitHub API NSString *urlString = @"https://api.github.com/"; NSURL *baseURL = [NSURL URLWithString:urlString]; AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL]; ! // The manager should serialize the response as JSON manager.requestSerializer = [AFJSONRequestSerializer serializer]; ! // Send a request to GET /users/:username/repos [manager GET:[NSString stringWithFormat:@"users/%@/repos", username] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // Send response object to success block success(responseObject); } failure:nil]; }

テストを成功させる /// GHVAPIClient.m ! // Create a request operation manager pointing at the GitHub API NSString *urlString = @"https://api.github.com/"; NSURL *baseURL = [NSURL URLWithString:urlString]; AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL]; ! // The manager should serialize the response as JSON manager.requestSerializer = [AFJSONRequestSerializer serializer]; ! // Send a request to GET /users/:username/repos [manager GET:[NSString stringWithFormat:@"users/%@/repos", username] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // Send response object to success block success(responseObject); } failure:nil]; }

テストを成功させる /// GHVAPIClient.m ! // Create a request operation manager pointing at the GitHub API NSString *urlString = @"https://api.github.com/"; NSURL *baseURL = [NSURL URLWithString:urlString]; AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL]; ! // The manager should serialize the response as JSON manager.requestSerializer = [AFJSONRequestSerializer serializer]; ! // Send a request to GET /users/:username/repos [manager GET:[NSString stringWithFormat:@"users/%@/repos", username] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // Send response object to success block success(responseObject); } failure:nil]; }

テストを成功させる /// GHVAPIClient.m ! // Create a request operation manager pointing at the GitHub API NSString *urlString = @"https://api.github.com/"; NSURL *baseURL = [NSURL URLWithString:urlString]; AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL]; ! // The manager should serialize the response as JSON manager.requestSerializer = [AFJSONRequestSerializer serializer]; ! // Send a request to GET /users/:username/repos [manager GET:[NSString stringWithFormat:@"users/%@/repos", username] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // Send response object to success block success(responseObject); } failure:nil]; }

このテストの問題点 • 外部依存 • GitHub APIが落ちていたらテストが失敗する • ネットに繋がっていなかったら失敗する • レスポンスが遅いと失敗する • 遅い • テストを実行するたびにリクエストが送られる

NocillaによるHTTP Stubbing 外部依存の除去 stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];

NocillaによるHTTP Stubbing 外部依存の除去 stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];

NocillaによるHTTP Stubbing 外部依存の除去 stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];

NocillaによるHTTP Stubbing 外部依存の除去 stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];

NocillaによるHTTP Stubbing 外部依存の除去 stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];

NocillaによるHTTP Stubbing 外部依存の除去 stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];

NocillaによるHTTP Stubbing 当たらなければ どうということはない

Nocillaで解決できた問題点 • 外部依存 • GitHub APIが落ちていたらテストが失敗する • ネットに繋がっていなかったら失敗する • レスポンスが遅いと失敗する • 遅い • テストを実行するたびにリクエストが送られる

Nocillaで解決できた問題点 • 外部依存 ✓• GitHub APIが落ちていたらテストが失敗する • ネットに繋がっていなかったら失敗する • レスポンスが遅いと失敗する • 遅い • テストを実行するたびにリクエストが送られる

Nocillaで解決できた問題点 • 外部依存 ✓• GitHub APIが落ちていたらテストが失敗する ✓• ネットに繋がっていなかったら失敗する • レスポンスが遅いと失敗する • 遅い • テストを実行するたびにリクエストが送られる

Nocillaで解決できた問題点 • 外部依存 ✓• GitHub APIが落ちていたらテストが失敗する ✓• ネットに繋がっていなかったら失敗する ✓• レスポンスが遅いと失敗する • 遅い • テストを実行するたびにリクエストが送られる

Nocillaで解決できた問題点 • 外部依存 ✓• GitHub APIが落ちていたらテストが失敗する ✓• ネットに繋がっていなかったら失敗する ✓• レスポンスが遅いと失敗する • 遅い ✓• テストを実行するたびにリクエストが送られる

他のNocillaの機能

他のNocillaの機能 • 正規表現を使ってHTTP requestのstub

他のNocillaの機能 • 正規表現を使ってHTTP requestのstub stubRequest(@"GET", @"https://api.github.com/" @"users/(.*?)/repos".regex)

他のNocillaの機能 • 正規表現を使ってHTTP requestのstub stubRequest(@"GET", @"https://api.github.com/" @"users/(.*?)/repos".regex) • ネットに繋がっていないときのエラーをテストする こともできる

他のNocillaの機能 • 正規表現を使ってHTTP requestのstub stubRequest(@"GET", @"https://api.github.com/" @"users/(.*?)/repos".regex) • ネットに繋がっていないときのエラーをテストする こともできる NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:29 userInfo:@{NSLocalizedDescriptionKey: @"Uh-oh!"}]; stubRequest(@"GET", @"...") .andFailWithError(error);

要約

要約 • 読みやすくて、非同期コードでも使えるBDDフレー ムワークKiwi • https://github.com/allending/Kiwi

要約 • 読みやすくて、非同期コードでも使えるBDDフレー ムワークKiwi • https://github.com/allending/Kiwi pod "Kiwi/XCTest"

要約 • 読みやすくて、非同期コードでも使えるBDDフレー ムワークKiwi • https://github.com/allending/Kiwi pod "Kiwi/XCTest" • 通信に依存するコードのテストに役立つNocilla • https://github.com/luisobo/Nocilla

要約 • 読みやすくて、非同期コードでも使えるBDDフレー ムワークKiwi • https://github.com/allending/Kiwi pod "Kiwi/XCTest" • 通信に依存するコードのテストに役立つNocilla • https://github.com/luisobo/Nocilla pod "Nocilla"

質疑応答 @modocache #startup_ios

質疑応答 @modocache #startup_ios describe(@"このLT", ^{ context(@"スライドが終わったら", ^{ it(@"質疑応答に移る", ^{ }); }); }); [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)];

質疑応答 @modocache #startup_ios describe(@"このLT", ^{ context(@"スライドが終わったら", ^{ it(@"質疑応答に移る", ^{ }); }); }); [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)];

質疑応答 @modocache #startup_ios describe(@"このLT", ^{ context(@"スライドが終わったら", ^{ it(@"質疑応答に移る", ^{ }); }); }); [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)];

質疑応答 @modocache #startup_ios describe(@"このLT", ^{ context(@"スライドが終わったら", ^{ it(@"質疑応答に移る", ^{ }); }); }); [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)];

質疑応答 @modocache #startup_ios describe(@"このLT", ^{ context(@"スライドが終わったら", ^{ it(@"質疑応答に移る", ^{ }); }); }); [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)];

Add a comment

Related presentations