Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

iOSビヘイビア駆動開発

10,879 views

Published on

簡単なサンプルアプリをBDDで開発しましょう。KiwiとNocillaを使って簡潔で速いテストの書き方を説明します。

Published in: Technology
  • Be the first to comment

iOSビヘイビア駆動開発

  1. 1. iOS ビヘイビア駆動開発 KiwiとNocillaでRESTfulなアプリのテスト 2014年3月9日 Brian Gesiak 研究生、東京大学 @modocache #startup_ios
  2. 2. 内容 • ビヘイビア駆動開発(BDD)とは • iOSでBDDを実践する例 • Kiwi • 非同期通信(HTTP)のテスト方法 • Nocilla
  3. 3. 通信のBDD サンプルアプリ • あるGitHubユーザのレポジトリを 表示するアプリを作りたい • GitHub APIからJSONをGETできる • https://api.github.com/users/ {{ username }}/repos.json
  4. 4. 通信のBDD サンプルアプリ /// GET /users/:username/repos ! [ { "id": 1296269, "name": "Hello-World", "description": "My first repo!", /* ... */ } ]
  5. 5. BDDで作ってみましょう Kiwiを使ってBDD実践 • ビヘイビア駆動開発とはテスト駆動開発から派生し た概念
  6. 6. テスト駆動開発とは Red-Green-Refactorサイクル • Red: ! • Green: ! • Refactor:
  7. 7. テスト駆動開発とは Red-Green-Refactorサイクル • Red: 失敗するテストを書く ! • Green: ! • Refactor:
  8. 8. テスト駆動開発とは Red-Green-Refactorサイクル • Red: 失敗するテストを書く ! • Green: テストが成功するようにコードを書く ! • Refactor:
  9. 9. テスト駆動開発とは Red-Green-Refactorサイクル • Red: 失敗するテストを書く ! • Green: テストが成功するようにコードを書く ! • Refactor: 重複をなくす
  10. 10. テスト駆動開発とは Red-Green-Refactorサイクル • Red: 失敗するテストを書く ! • Green: テストが成功するようにコードを書く ! • Refactor: 重複をなくす 繰り返す
  11. 11. XCTestを使ったiOS TDDの一例
  12. 12. 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
  13. 13. 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
  14. 14. 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
  15. 15. 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
  16. 16. 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
  17. 17. 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
  18. 18. XCTestを使ったiOS TDDの一例 あえて言おう! カスであると!
  19. 19. ビヘイビア駆動開発(BDD) • 「何をテストすればいいのか」 • コードをテストするのではなく、求めている挙動を 説明(specify)する
  20. 20. KiwIを使ったiOS BDDの一例
  21. 21. 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]; }); }); }); });
  22. 22. 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]; }); }); }); });
  23. 23. 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]; }); }); }); });
  24. 24. 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]; }); }); }); });
  25. 25. 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]; }); }); }); });
  26. 26. 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]; }); }); }); });
  27. 27. Kiwiのメリット
  28. 28. Kiwiのメリット • Setup、teardownを無限にネストできる
  29. 29. Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); });
  30. 30. Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ • Mock、stubも入っている /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); });
  31. 31. Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mock、stubも入っている [collection stub:@selector(addRepo:)];
  32. 32. Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mock、stubも入っている [collection stub:@selector(addRepo:)]; • 非同期テストをサポート
  33. 33. Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mock、stubも入っている [collection stub:@selector(addRepo:)]; • 非同期テストをサポート [[collection.repos shouldEventually] haveCountOf:2];
  34. 34. Kiwiのメリット • Setup、teardownを無限にネストできる beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mock、stubも入っている [collection stub:@selector(addRepo:)]; • 非同期テストをサポート [[collection.repos shouldEventually] haveCountOf:2]; • XCTestより読みやすい
  35. 35. まずは失敗するテストを
  36. 36. まずは失敗するテストを /// 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]; });
  37. 37. まずは失敗するテストを /// 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]; });
  38. 38. まずは失敗するテストを /// 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]; });
  39. 39. まずは失敗するテストを /// 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]; });
  40. 40. まずは失敗するテストを /// 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]; });
  41. 41. まずは失敗するテストを /// 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]; });
  42. 42. テストを成功させる
  43. 43. テストを成功させる /// 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]; }
  44. 44. テストを成功させる /// 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]; }
  45. 45. テストを成功させる /// 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]; }
  46. 46. テストを成功させる /// 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]; }
  47. 47. テストを成功させる /// 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]; }
  48. 48. このテストの問題点 • 外部依存 • GitHub APIが落ちていたらテストが失敗する • ネットに繋がっていなかったら失敗する • レスポンスが遅いと失敗する • 遅い • テストを実行するたびにリクエストが送られる
  49. 49. 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];
  50. 50. 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];
  51. 51. 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];
  52. 52. 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];
  53. 53. 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];
  54. 54. 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];
  55. 55. NocillaによるHTTP Stubbing 当たらなければ どうということはない
  56. 56. Nocillaで解決できた問題点 • 外部依存 • GitHub APIが落ちていたらテストが失敗する • ネットに繋がっていなかったら失敗する • レスポンスが遅いと失敗する • 遅い • テストを実行するたびにリクエストが送られる
  57. 57. Nocillaで解決できた問題点 • 外部依存 ✓• GitHub APIが落ちていたらテストが失敗する • ネットに繋がっていなかったら失敗する • レスポンスが遅いと失敗する • 遅い • テストを実行するたびにリクエストが送られる
  58. 58. Nocillaで解決できた問題点 • 外部依存 ✓• GitHub APIが落ちていたらテストが失敗する ✓• ネットに繋がっていなかったら失敗する • レスポンスが遅いと失敗する • 遅い • テストを実行するたびにリクエストが送られる
  59. 59. Nocillaで解決できた問題点 • 外部依存 ✓• GitHub APIが落ちていたらテストが失敗する ✓• ネットに繋がっていなかったら失敗する ✓• レスポンスが遅いと失敗する • 遅い • テストを実行するたびにリクエストが送られる
  60. 60. Nocillaで解決できた問題点 • 外部依存 ✓• GitHub APIが落ちていたらテストが失敗する ✓• ネットに繋がっていなかったら失敗する ✓• レスポンスが遅いと失敗する • 遅い ✓• テストを実行するたびにリクエストが送られる
  61. 61. 他のNocillaの機能
  62. 62. 他のNocillaの機能 • 正規表現を使ってHTTP requestのstub
  63. 63. 他のNocillaの機能 • 正規表現を使ってHTTP requestのstub stubRequest(@"GET", @"https://api.github.com/" @"users/(.*?)/repos".regex)
  64. 64. 他のNocillaの機能 • 正規表現を使ってHTTP requestのstub stubRequest(@"GET", @"https://api.github.com/" @"users/(.*?)/repos".regex) • ネットに繋がっていないときのエラーをテストする こともできる
  65. 65. 他の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);
  66. 66. 要約
  67. 67. 要約 • 読みやすくて、非同期コードでも使えるBDDフレー ムワークKiwi • https://github.com/allending/Kiwi
  68. 68. 要約 • 読みやすくて、非同期コードでも使えるBDDフレー ムワークKiwi • https://github.com/allending/Kiwi pod "Kiwi/XCTest"
  69. 69. 要約 • 読みやすくて、非同期コードでも使えるBDDフレー ムワークKiwi • https://github.com/allending/Kiwi pod "Kiwi/XCTest" • 通信に依存するコードのテストに役立つNocilla • https://github.com/luisobo/Nocilla
  70. 70. 要約 • 読みやすくて、非同期コードでも使えるBDDフレー ムワークKiwi • https://github.com/allending/Kiwi pod "Kiwi/XCTest" • 通信に依存するコードのテストに役立つNocilla • https://github.com/luisobo/Nocilla pod "Nocilla"
  71. 71. 質疑応答 @modocache #startup_ios
  72. 72. 質疑応答 @modocache #startup_ios describe(@"このLT", ^{ context(@"スライドが終わったら", ^{ it(@"質疑応答に移る", ^{ }); }); }); [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)];
  73. 73. 質疑応答 @modocache #startup_ios describe(@"このLT", ^{ context(@"スライドが終わったら", ^{ it(@"質疑応答に移る", ^{ }); }); }); [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)];
  74. 74. 質疑応答 @modocache #startup_ios describe(@"このLT", ^{ context(@"スライドが終わったら", ^{ it(@"質疑応答に移る", ^{ }); }); }); [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)];
  75. 75. 質疑応答 @modocache #startup_ios describe(@"このLT", ^{ context(@"スライドが終わったら", ^{ it(@"質疑応答に移る", ^{ }); }); }); [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)];
  76. 76. 質疑応答 @modocache #startup_ios describe(@"このLT", ^{ context(@"スライドが終わったら", ^{ it(@"質疑応答に移る", ^{ }); }); }); [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)];

×