Upgrade to Pro — share decks privately, control downloads, hide ads and more …

iOSアプリ開発における自動化ノウハウ

 iOSアプリ開発における自動化ノウハウ

Kishikawa Katsumi

March 01, 2015
Tweet

More Decks by Kishikawa Katsumi

Other Decks in Technology

Transcript

  1. มߋ มߋ มߋ มߋ มߋ W ϦϦʔε ख࡞ۀͷʢ͚ͩͲΑ͋͘ΔʣϦϦʔεϓϩηε HJUQVMM "MSFBEZVQUPEBUF

    2. खݩͷ؀ڥΛ࠷৽ʹߋ৽ͯ͠ 1. ϦϞʔτϦϙδτϦͷߋ৽ΛࢭΊͯ 3. όʔδϣϯ൪߸Λߋ৽ͯ͠
  2. มߋ มߋ มߋ มߋ มߋ W ਃ੥ ख࡞ۀͷʢ͚ͩͲΑ͋͘ΔʣϦϦʔεϓϩηε HJUQVMM 6QEBUJOHFFCE

    FSSPS:PVSMPDBMDIBOHFTUPUIFGPMMPXJOHpMFTXPVMECFPWFSXSJUUFOCZNFSHF 6CJSFHJ"QQ%FMFHBUFN 1MFBTF DPNNJUZPVSDIBOHFTPSTUBTIUIFNCFGPSFZPVDBONFSHF "CPSUJOH 2. खݩͷ؀ڥΛ࠷৽ʹߋ৽ͯ͠ 1. ϦϞʔτϦϙδτϦͷߋ৽ΛࢭΊͯ ߋ৽ʹࣦഊͨ͠ͷʹؾ͔ͮͳ͔ͬͨ
  3. มߋ มߋ มߋ มߋ มߋ W ਃ੥ ख࡞ۀͷʢ͚ͩͲΑ͋͘ΔʣϦϦʔεϓϩηε HJUGFUDI 

    2. खݩͷ؀ڥΛ࠷৽ʹߋ৽ͯ͠ 1. ϦϞʔτϦϙδτϦͷߋ৽ΛࢭΊͯ HJUGFUDI͚ͩͯ͠NFSHFΛ๨Εͨ
  4. มߋ มߋ มߋ มߋ มߋ W ਃ੥ ख࡞ۀͷʢ͚ͩͲΑ͋͘ΔʣϦϦʔεϓϩηε HJUQVMM "MSFBEZVQUPEBUF

    2. खݩͷ؀ڥΛ࠷৽ʹߋ৽ͯ͠ 1. ϦϞʔτϦϙδτϦͷߋ৽ΛࢭΊͯ ࣮͸खݩͷํ͕ϦϞʔτΑΓਐΜͰ͍ͨ
  5. • खݩͷ؀ڥ͕ϦϞʔτͱҟͳ͍ͬͯͨ • Ϗϧυߏ੒͕ҧ͍ͬͯͨ • Provisioning Profile͕ҧ͏ • όʔδϣϯ൪߸Λ͚ͭؒҧ͑ͨ •

    ϦδΣΫτ͞Εͨ৔߹ɺͲ͜ʹ໭Δ͔ • etc… ख࡞ۀͷϦϦʔεϓϩηεʹ͓͚Δࣦഊྫ
  6. w ʮࣗಈԽͰ͖Δঢ়ଶʯΛอͭ͜ͱ͕ॏཁ w ʮࣗಈԽͰ͖Δঢ়ଶʯͱ͸λεΫ͕γϯϓϧʹ෼ׂ ͞ΕͯػցͰ΋࣮ߦՄೳͰ͋Δͱ͍͏͜ͱ w ػցͰ࣮ߦͰ͖ΔͳΒɺ܁Γฦͨ͠Γɺه࿥΍࿈ܞ ͕༰қͰ͋Δ w ୭͕ݟͯ΋ঢ়ଶ͕೺ѲͰ͖ΔΑ͏ʹͳΔ

    w ࣗಈԽ͕໨తͰ͸ͳͯ͘ɺࣗಈԽͰ͖͍ͯΔͳΒʮࣗ ಈԽͰ͖Δঢ়ଶʯ͕ҡ࣋Ͱ͖͍ͯΔͱ͍͏͜ͱ w ࣗಈԽʹΑͬͯʮࣗಈԽͰ͖Δঢ়ଶʯ͕ҡ࣋͞Εͯ Δ͜ͱΛอূ͢Δ
  7. • มߋ؅ཧ • Github • Pull Request • ίʔυϨϏϡʔ •

    ϥΠϒϥϦ؅ཧ • ࣗಈϏϧυ • ςετ • ςετͷࣗಈԽ • ܧଓతΠϯςάϨʔγϣϯʢCIʣ • ίʔυΧόϨοδ ࣗಈԽ΁ͷऔΓ૊Έ
  8. • ෼ࢄܕόʔδϣϯ؅ཧγεςϜ • Git → Github, Bitbucket • Mercurial →

    Bitbucket มߋ؅ཧ Github͔BitbucketΛҰॹʹ࢖͓͏ʂ
  9. source 'https://github.com/CocoaPods/Specs.git' source 'https://[email protected]/ubiregiinc/Specs.git' platform :ios, '7.0' inhibit_all_warnings! pod 'UUIDShortener'

    pod 'JLRoutes' pod 'NLCoreData' pod 'AFNetworking' pod 'AFNetworkActivityLogger' pod 'SDWebImage' pod 'PEPhotoCropEditor' pod 'UICKeyChainStore' pod 'GCDWebServer', '~> 1.4' pod 'FXImageView' pod 'FastCoding' pod 'SSZipArchive' pod 'Helpshift' pod 'CrittercismSDK' pod 'Aspects' pod 'GoogleAnalytics-iOS-SDK' target :Ubiregi2Tests do pod 'OCMock' pod 'OCHamcrest' pod 'OHHTTPStubs' end
  10. PODS: - AFNetworkActivityLogger (2.0.3): - AFNetworking/NSURLConnection (~> 2.0) - AFNetworking/NSURLSession

    (~> 2.0) - AFNetworking (2.5.0): - AFNetworking/NSURLConnection (= 2.5.0) - AFNetworking/NSURLSession (= 2.5.0) - AFNetworking/Reachability (= 2.5.0) - AFNetworking/Security (= 2.5.0) - AFNetworking/Serialization (= 2.5.0) - AFNetworking/UIKit (= 2.5.0) - Aspects (1.4.1) - CoineyKit-iOS (0.0.7) - CrittercismSDK (5.0.7) - FastCoding (3.2) - FXImageView (1.3.5) - GCDWebServer (1.4) - GoogleAnalytics-iOS-SDK (3.10): - GoogleAnalytics-iOS-SDK/Core (= 3.10) - GoogleAnalytics-iOS-SDK/Core (3.10) - Helpshift (4.9.1) - JLRoutes (1.5.1) - M13BadgeView (1.0.4) - NLCoreData (0.5.3) - OCHamcrest (4.1.1) - OCMock (3.1.2) - OHHTTPStubs (3.1.10): - OHHTTPStubs/Core (= 3.1.10) - OHHTTPStubs/Core (3.1.10) - PEPhotoCropEditor (1.3.1) - SDWebImage (3.7.1): - SDWebImage/Core (= 3.7.1) - SDWebImage/Core (3.7.1) - SEJSONViewController (0.2.0) - SSZipArchive (0.3.2) - UICKeyChainStore (2.0.2) - UUIDShortener (1.0.1) - YLProgressBar (3.5.1)
  11. • WebαʔϏε • Travis CI • Circle CI • Greenhouse

    CI • ιϑτ΢ΣΞ • Jenkins มߋ؅ཧ
  12. xcodebuild test -workspace Ubiregi2.xcworkspace -scheme Ubiregi2-Release -sdk iphonesimulator -configuration Release

    -derivedDataPath build -destination name=iPad 2,OS=7.1 -destination name=iPad Air,OS=8.1 CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO GCC_SYMBOLS_PRIVATE_EXTERN=NO 1. γϛϡϨʔλ༻ʹϏϧυͯ͠ɺςετΛ࣮ߦ ίϚϯυϥΠϯ • Ϗϧυߏ੒(Debug/Release)͸Ҿ਺Ͱࢦఆ • destinationύϥϝʔλͰ࣮ߦ؀ڥΛࢦఆͰ͖Δ • GCC_SYMBOLS_PRIVATE_EXTERNύϥϝʔλΛҾ਺Ͱ্ॻ͖
  13. $ xcodebuild build -workspace Ubiregi2.xcworkspace -scheme Ubiregi2-Release -configuration Release -derivedDataPath

    "build" CONFIGURATION_TEMP_DIR="build/temp" CODE_SIGN_IDENTITY="iPhone Distribution: Ubiregi Inc. (A1234567XZ)" 2. σόΠε༻ʹϏϧυ͢Δɻ ίϚϯυϥΠϯ • ϓϩϏδϣχϯάϓϩϑΝΠϧ͸Ҿ਺Ͱࢦఆ ʢෳ਺ͷλʔήοτ͕͋Δ৔߹͸.xcodeprojͰࢦఆ͢Δʣ
  14. $ xcodebuild archive -workspace Ubiregi2.xcworkspace -scheme Ubiregi2-Release -configuration Release -derivedDataPath

    "build" CONFIGURATION_TEMP_DIR="build/temp" CODE_SIGN_IDENTITY="iPhone Distribution: Ubiregi Inc. (A1234567XZ)" -archivePath "build/Ubiregi2-Release" 3. ϦϦʔεߏ੒ͰϏϧυͯ͠ɺ.xcarchiveʢਃ੥༻ͷΞʔΧΠϒʣΛ࡞Δ ίϚϯυϥΠϯ • ϓϩϏδϣχϯάϓϩϑΝΠϧ͸Ҿ਺Ͱࢦఆ ʢෳ਺ͷλʔήοτ͕͋Δ৔߹͸.xcodeprojͰࢦఆ͢Δʣ
  15. $ xcodebuild -exportArchive -archivePath "build/Ubiregi2-Release.xcarchive" -exportFormat "IPA" -exportPath "build/Ubiregi2-AdHoc.ipa" -exportProvisioningProfile

    "Ubiregi AdHoc" 4. ਃ੥༻ͷϏϧυ͔ΒɺϓϩϏδϣχϯάϓϩϑΝΠϧ͚ͩม͑ͯ ݕূ༻ͷϕʔλ൛ͷ.ipaΛ࡞੒͢Δ ʢਃ੥ͨ͠΋ͷͱॺ໊Ҏ֎͸શ͘ಉ͡όΠφϦͰݕূͰ͖Δʣ ίϚϯυϥΠϯ
  16. $ xcodebuild -exportArchive -archivePath "build/Ubiregi2-Release.xcarchive" -exportFormat "IPA" -exportPath "build/Ubiregi2-Release-AppStore.ipa" -exportProvisioningProfile

    "Ubiregi App Store" 5. ਃ੥༻ͷϏϧυ͔ΒɺApplication LoaderͰΞοϓϩʔυ͢ΔͨΊʹ.ipaΛ࡞੒͢Δ ίϚϯυϥΠϯ
  17. def build_options(opt = {configuration: "Release"}) options = { sdk: "iphoneos",

    workspace: "Ubiregi2-Release.xcworkspace", scheme: "Ubiregi2-Release", }.merge opt join_options(options: options, prefix: "-", separator: " ") end def build_settings(configuration: "Release") settings = { CONFIGURATION_BUILD_DIR: "build", CONFIGURATION_TEMP_DIR: "build/temp", CODE_SIGN_IDENTITY: "iPhone Distribution: Ubiregi Inc. (Y7522692LT)", } settings = join_options(options: settings, prefix: "", separator: "=") end def test(configuration: "Release") options = build_options(sdk: "iphonesimulator", configuration: configuration) options << DESTINATIONS.map { |destination| %(-destination "#{destination}") }.join(" ") sh "xcodebuild #{options} test | xcpretty -c" end def build_xcarchive(configuration: "Release") options = build_options(configuration: configuration) settings = build_settings(configuration: configuration) sh "xcodebuild #{options} #{settings} archive -archivePath #{ARCHIVE_FILE} | xcpretty -c" end def export_ipa sh "xcodebuild -exportArchive -exportFormat IPA -archivePath #{ARCHIVE_FILE} -exportPath #{IPA_FILE} | xcpretty -c" end ֤ίϚϯυΛλεΫʹ͢Δ Rake
  18. release_date = DateTime.now.strftime("%Y/%m/%d %H:%M:%S") release_notes = "Build: #{InfoPlist.marketing_and_build_version} \nUploaded: #{release_date}"

    fields = { file: "@#{IPA_FILE}", api_token: API_TOKEN, team_token: TEAM_TOKEN, notes: release_notes, notify: true, distribution_lists: DISTRIBUTION_LISTS } fields = join_option(options: fields, prefix: "-F ", seperator: "=") sh "curl -sL -w \"%{http_code} %{url_effective}\\n\" http:// testflightapp.com/api/builds.json #{fields} -o /dev/null ֤ίϚϯυΛλεΫʹ͢Δ Rake
  19. release_date = DateTime.now.strftime("%Y/%m/%d %H:%M:%S") release_notes = "Build: #{InfoPlist.marketing_and_build_version} \nUploaded: #{release_date}"

    fields = { file: "@#{IPA_FILE}", api_token: API_TOKEN, team_token: TEAM_TOKEN, notes: release_notes, notify: true, distribution_lists: DISTRIBUTION_LISTS } fields = join_option(options: fields, prefix: "-F ", seperator: "=") sh "curl -sL -w \"%{http_code} %{url_effective}\\n\" http:// testflightapp.com/api/builds.json #{fields} -o /dev/null ֤ίϚϯυΛλεΫʹ͢Δ Rake
  20. XCJobs::Test.new do |t| t.workspace = 'Ubiregi2' t.scheme = 'Ubiregi2-Release' t.configuration

    = 'Release' t.build_dir = 'build' t.add_destination('name=iPad 2,OS=7.1') t.add_destination('name=iPad Air,OS=8.1') end xcodebuild test -workspace Ubiregi2.xcworkspace -scheme Ubiregi2-Release -sdk iphonesimulator -configuration Release -derivedDataPath build -destination name=iPad 2,OS=7.1 -destination name=iPad Air,OS=8.1 CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO GCC_SYMBOLS_PRIVATE_EXTERN=NO $ rake test 1. γϛϡϨʔλ༻ʹϏϧυͯ͠ɺςετΛ࣮ߦ
  21. XCJobs::Archive.new do |t| t.workspace = APP_NAME t.scheme = SCHEME t.configuration

    = configuration t.signing_identity = signing_identity t.build_dir = BUILD_DIR t.formatter = 'xcpretty -c' end xcodebuild archive -workspace Ubiregi2.xcworkspace -scheme Ubiregi2-Release -configuration Release -derivedDataPath "build" CONFIGURATION_TEMP_DIR="build/temp" CODE_SIGN_IDENTITY="iPhone Distribution: Ubiregi Inc. (A1234567XZ)" -archivePath "build/Ubiregi2-Release" $ rake build:archive 3. ϦϦʔεߏ੒ͰϏϧυͯ͠ɺ.xcarchiveʢਃ੥༻ͷΞʔΧΠϒʣΛ࡞Δ
  22. XCJobs::Export.new do |t| t.archive_path = File.join('build', 'Ubiregi2.xcarchive') t.export_path = File.join('build',

    'Ubiregi2.ipa') t.export_provisioning_profile = 'Ubiregi_Ad_Hoc.mobileprovision' t.formatter = 'xcpretty -c' end xcodebuild -exportArchive -archivePath "build/Ubiregi2.xcarchive" -exportFormat "IPA" -exportPath "build/Ubiregi2-AdHoc.ipa" -exportProvisioningProfile "Ubiregi AdHoc" $ rake build:export 4. ਃ੥༻ͷϏϧυ͔ΒɺϓϩϏδϣχϯάϓϩϑΝΠϧ͚ͩม͑ͯ ݕূ༻ͷϕʔλ൛ͷ.ipaΛ࡞੒͢Δ
  23. language: objective-c install: - bundle install - bundle exec pod

    install script: - bundle exec rake test profiles:install certificates:install version:set_build_version build:archive build:export distribute:testflight CIͱ؆୯ʹ࿈ܞͰ͖Δ
  24. XCJobs::Test.new do |t| t.workspace = 'UCZProgressView' t.scheme = 'UCZProgressView' t.configuration

    = 'Release' t.build_dir = 'build' t.coverage = true t.formatter = 'xcpretty -c' end XCJobs::Coverage::Coveralls.new() do |t| t.add_extension('.m') t.add_exclude('Example') end ؆୯ͳϓϩδΣΫτͷ৔߹
  25. language: objective-c install: - bundle install script: - bundle exec

    rake test coverage:coveralls ؆୯ͳϓϩδΣΫτͷ৔߹
  26. Ϣχοτςετ /* "vat" : 5 "price" : "2000.0", "price_type" :

    "intax" */ UBCheckoutItem *checkoutItem = nil; for (checkoutItem in self.checkoutItems) { if (checkoutItem.checkoutMenuItem.isTaxIncluded) { break; } } checkoutItem.discount = [NSDecimalNumber decimalNumberWithString:@"500"]; checkoutItem.count = @(1); XCTAssertEqualObjects(checkoutItem.apiSalesNet, [NSDecimalNumber decimalNumberWithString:@"1905"]); XCTAssertEqualObjects(checkoutItem.apiSalesTax, [NSDecimalNumber decimalNumberWithString:@"95"]); XCTAssertEqualObjects(checkoutItem.apiDiscountNet, [NSDecimalNumber decimalNumberWithString:@"476"]); XCTAssertEqualObjects(checkoutItem.apiDiscountTax, [NSDecimalNumber decimalNumberWithString:@"24"]); XCTAssertEqualObjects(checkoutItem.discountRatio, [NSDecimalNumber decimalNumberWithString:@"25"]); checkoutItem.count = @(2); XCTAssertEqualObjects(checkoutItem.apiSalesNet, [NSDecimalNumber decimalNumberWithString:@"3810"]); XCTAssertEqualObjects(checkoutItem.apiSalesTax, [NSDecimalNumber decimalNumberWithString:@"190"]); XCTAssertEqualObjects(checkoutItem.apiDiscountNet, [NSDecimalNumber decimalNumberWithString:@"476"]); XCTAssertEqualObjects(checkoutItem.apiDiscountTax, [NSDecimalNumber decimalNumberWithString:@"24"]); ֹۚͷςετ
  27. Ϣχοτςετ NSManagedObjectContext *context = [NSManagedObjectContext mainContext]; NSMutableOrderedSet *checkoutItems = [[NSMutableOrderedSet

    alloc] init]; UBCheckoutItem *item; item = [UBCheckoutItem checkoutItemWithMenuItem:self.menuItems[@"intaxItem"]]; item.count = @(1); item.discount = [NSDecimalNumber decimalNumberWithString:@"500"]; [checkoutItems addObject:item]; item = [UBCheckoutItem checkoutItemWithMenuItem:self.menuItems[@"outtaxItem"]]; item.count = @(1); item.discount = [NSDecimalNumber decimalNumberWithString:@"190"]; [checkoutItems addObject:item]; item = [UBCheckoutItem checkoutItemWithMenuItem:self.menuItems[@"nosalesItem"]]; item.count = @(1); item.discount = [NSDecimalNumber decimalNumberWithString:@"-500"]; [checkoutItems addObject:item]; item = [UBCheckoutItem checkoutItemWithMenuItem:self.menuItems[@"percentageItem"]]; item.count = @(1); [checkoutItems addObject:item]; UBCheckout *checkout = [UBCheckout insertInContext:context]; checkout.checkoutItems = checkoutItems; XCTAssertEqualObjects(checkout.total, [NSDecimalNumber decimalNumberWithString:@"3737"], ɹɹɹɹɹɹɹɹɹɹɹɹɹ@"Ϣʔβʔʹ੥ٻ͞ΕΔֹۚ"); XCTAssertEqualObjects(checkout.tax, [NSDecimalNumber decimalNumberWithString:@“192"], ɹɹɹɹɹɹɹɹɹɹɹɹɹ @"಺ফඅ੫ֹ"); ֹۚͷςετ
  28. Ϣχοτςετ NSDictionary *response = [UBTestUtils JSONObjectWithPath:self.dataPath]; [UBTestUtils stubWithJSONObject:response path:self.endpoint]; NSManagedObjectContext

    *context = [NSManagedObjectContext mainContext]; UBAccount *account = [UBAccount currentAccountInContext:context]; UBAccountSynchronizer *synchronizer = [[UBAccountSynchronizer alloc] initWithAccountID:account.identifier]; XCTestExpectation *expectation = [self expectationWithDescription:@"finish synchronization"]; synchronizer.completionHandler = ^(NSError *error) { assertThat(error, nilValue()); [expectation fulfill]; }; [synchronizer synchronize]; [self waitForExpectationsWithTimeout:10.0 handler:nil]; [context refreshObject:account mergeChanges:YES]; NSDictionary *result = account.dictionaryRepresentation; NSDictionary *expected = response[@"account"]; XCTAssertEqualObjects(result[@"id"], expected[@"id"]); XCTAssertEqualObjects(result[@"login"], expected[@"login"]); XCTAssertEqualObjects(result[@"name"], expected[@"name"]); XCTAssertEqualObjects(result[@"email"], expected[@"email"]); ௨৴ͷςετ
  29. Ϣχοτςετ + (id)stubWithJSONObject:(id)JSONObject statusCode:(int)statusCode path: (NSString *)path { id<OHHTTPStubsDescriptor> stub

    = nil; stub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { return [request.URL.path hasPrefix:path]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { return [[OHHTTPStubsResponse responseWithJSONObject:JSONObject statusCode:statusCode headers:nil] requestTime:1.0 responseTime:1.0]; }]; return stub; } ௨৴Λελϒʹ͢Δ
  30. • ςετ͸Πϯϑϥ • ؀ڥΛ੔͑Δ͜ͱ͕େࣄ • جຊతʹ໘౗ͳͷͰࣗಈతʹ࣮ߦ͞ΕΔ࢓૊ ΈΛ࡞Δ • ఆظతʹ࣮ߦ͢Δ •

    ॻ͖΍͍͢ͱ͜Ζ͔Βॻ͘ • େࣄͳͱ͜Ζ͔Βॻ͘ • ॻ͖΍͍͢Α͏ͳઃܭΛ͜͜Ζ͕͚Δ Ϣχοτςετ
  31. XCJobs::Test.new do |t| t.workspace = 'Ubiregi2' t.scheme = ‘Ubiregi2-Release' t.configuration

    = 'Release' t.build_dir = 'build' t.add_destination('name=iPad 2,OS=7.1') t.add_destination('name=iPad Air,OS=8.1') t.coverage = true t.formatter = 'xcpretty -c' end XCJobs::Coverage::Coveralls.new() do |t| t.service_name = 'travis-pro' t.repo_token = ‘xxx...’ t.add_extension('.m') t.add_exclude('Pods') t.add_exclude('Ubiregi2Tests') t.add_exclude_pattern('.*ViewController\.[hm]') end ίʔυΧόϨοδ XCJobsͰઃఆ͢Δ৔߹