Äú¿ÉÒÔ¾èÖú£¬Ö§³ÖÎÒÃǵĹ«ÒæÊÂÒµ¡£

1Ôª 10Ôª 50Ôª





ÈÏÖ¤Â룺  ÑéÖ¤Âë,¿´²»Çå³þ?Çëµã»÷Ë¢ÐÂÑéÖ¤Âë ±ØÌî



  ÇóÖª ÎÄÕ ÎÄ¿â Lib ÊÓÆµ iPerson ¿Î³Ì ÈÏÖ¤ ×Éѯ ¹¤¾ß ½²×ù Modeler   Code  
»áÔ±   
 
   
 
 
     
   
 ¶©ÔÄ
  ¾èÖú
iOSµ¥Ôª²âÊÔºÍUI²âÊÔÈ«Ãæ½âÎö
 
À´Ô´£º51CTO ·¢²¼ÓÚ£º 2017-4-20
  3408  次浏览      28
 

µ±½ñÊÀ½ç£¬²âÊÔ³ÉΪÈí¼þ¹¤³Ì¿ª·¢µÄ±ØÐè»·½Ú¡£¸ßÖÊÁ¿µÄµ¥Ôª²âÊÔÓëUI²âÊÔ¿ÉÒÔÈ·±£ÄúµÄÈí¼þÎÞºó¹ËÖ®ÓÇ£¬ÌرðÊǰÑһЩ¸ßÖÊÁ¿º¯Êý»òÕßÄ£¿éÄÉÈëÄúµÄ´úÂë²Ö¿âÖÐÖØÓÃ֮ʱ¡£ÔÚ±¾ÆªÖУ¬ÎÒÃǽ«ÏòÄúÈ«ÃæÏ¸ÖµؽéÉÜ»ùÓÚiOSƽ̨½øÐе¥Ôª²âÊÔºÍUI²âÊÔµÄÍêÕû¹ý³Ì¼°Ïà¹Ø¼¼ÇÉ¡£

±àд²âÊԿɲ»ÊÇÒ»ÏîÃÔÈ˵Ť×÷;È»¶ø£¬ÓÉÓÚ²âÊÔ¿ÉÒÔ±ÜÃâʹÄãµÄ±¦±´Ó¦ÓóÌÐò±ä³ÉÒ»¿é³ä³â´íÎóµÄ´óÀ¬»ø³¡£¬ËùÒÔ±àд²âÊÔÓÖÊÇÒ»Ïî·Ç³£ÓбØÒª×öµÄ¹¤×÷¡£Èç¹ûÄãÕýÔÚÔĶÁ±¾ÎÄ£¬ÄÇôÄãÓ¦µ±ÒѾ­ÖªµÀÄãÓ¦¸ÃΪÄúµÄ´úÂëºÍÓû§½çÃæ±àд²âÊÔ£¬Ö»ÊDz»È·¶¨ÈçºÎÔÚXcodeÖбàд²âÊÔ¡£

Ò²ÐíÄãÒѾ­¿ª·¢³öÒ»¸öÄܹ»¹¤×÷µÄÓ¦ÓóÌÐò£¬Ö»ÊÇ»¹Ã»ÓжÔËü½øÐвâÊÔ;ÁíÒ»·½Ã棬µ±ÄúÀ©Õ¹¸ÃÓ¦ÓóÌÐòʱ£¬ÄãÓÖÏë¶ÔÆäÈκεĸü¸Ä½øÐвâÊÔ¡£Ò²ÐíÄãÒѾ­Ð´ÁËһЩ²âÊÔ£¬µ«Éв»ÄÜÈ·¶¨ËüÃÇÊÇ·ñÊÇÕýÈ·µÄ²âÊÔ¡£»òÕߣ¬ÄãÏÖÔÚÕýÔÚ¿ª·¢ÄúµÄÓ¦ÓóÌÐò£¬²¢ÇÒÏëËæ×Ź¤×÷µÄ½øÕ¹¶ÔÖ®½øÐвâÊÔ¡£

±¾½Ì³Ì½«ÏòÄúÈ«ÃæÕ¹Ê¾ÈçºÎʹÓÃXcodeÖеIJâÊÔµ¼º½Æ÷À´²âÊÔÓ¦ÓóÌÐòµÄÄ£ÐͺÍÒì²½·½·¨£¬ÒÔ¼°ÈçºÎͨ¹ýʹÓôúÀí(×¢stub£¬ÓеÄÎÄÕÂÒë×÷¡°´æ¸ù¡±)ºÍÄ£Äâ(mock)À´Ä£·ÂÓë¿â»òϵͳ¶ÔÏóµÄ½»»¥£¬ÈçºÎ²âÊÔÓû§½çÃæºÍÐÔÄÜ£¬ÒÔ¼°ÈçºÎʹÓôúÂ븲¸Ç¹¤¾ß¡£Ëæ×ÅÎÄÕµÄÕ¹¿ª£¬Äã»á²»¶ÏÊìϤһЩÓë²âÊÔÏà¹ØµÄÊõÓµ½ÎÄÕ½áβʱÄã»á³Á×ŵذÑÒÀÀµ¹ØÏµ×¢Èëµ½ÄãµÄ±»²âϵͳ(SUT£¬system under test)ÖÐ!

²âÊÔ£¬²âÊÔ¡­¡­

²âÊÔʲô?

ÔÚдÈκβâÊÔ֮ǰ£¬Ê×ÏÈÒªÃ÷È·×î»ù±¾µÄÎÊÌâ©UÄãÐèÒª²âÊÔʲô?Èç¹ûÄãµÄÄ¿±êÊÇÀ©Õ¹Ò»¿îÏÖÓеÄÓ¦ÓóÌÐò£¬ÄÇôÄúÓ¦¸ÃÊ×ÏÈΪÄú¼Æ»®¸ü¸ÄµÄÈκÎ×é¼þ±àд²âÊÔ¡£

¸üÒ»°ãµÄÇé¿öÏ£¬ÄãµÄ²âÊÔÓ¦°üÀ¨ÈçÏÂһЩÄÚÈÝ©U

ºËÐŦÄÜ©UÄ£ÐÍÀàºÍ·½·¨¼°ÆäÓë¿ØÖÆÆ÷µÄ½»»¥

×î³£¼ûµÄÓû§½çÃæ¹¤×÷Á÷

±ß½çÌõ¼þ

´íÎóÐÞ¸´

µ±ÎñÖ®¼±

Ê××ÖĸËõÂÔ´ÊFIRSTÃèÊöÁËÒ»Ì×¼òÃ÷ÓÐЧµÄµ¥Ôª²âÊÔ±ê×¼¡£ÕâЩ±ê×¼ÊÇ©U

Fast(¿ìËÙ)©U²âÊÔµÄÔËÐÐËÙ¶ÈÓ¦¸ÃºÜ¿ì£¬ÕâÑùÒ»À´ÈËÃǾͲ»»á½éÒâÔËÐÐËüÃÇ¡£

Independent/Isolated(¶ÀÁ¢/·ÖÀë)©UÒ»¸ö²âÊÔ²»Ó¦ÒòÁíÒ»¸ö²âÊÔ¶ø½øÐа²×°»ò²ðж¡£

Repeatable(¿ÉÖØ¸´)©Uÿ´ÎÔËÐвâÊÔʱ£¬ÄúÓ¦¸Ã»ñµÃÏàͬµÄ½á¹û¡£ÖµµÃ×¢ÒâµÄÊÇ£¬ÍⲿÊý¾ÝÌṩÕߺͲ¢·¢ÎÊÌâ¿ÉÄܻᵼÖ³ÌÐòµÄ¼äЪÐÔ¹ÊÕÏ¡£

Self-validating(×ÔÎÒÑéÖ¤)©U²âÊÔÓ¦¸ÃÄܹ»ÍêÈ«×Ô¶¯»¯½øÐÐ;Êä³öÓ¦¸ÃҪôÊÇ¡°pass¡±(¼´¡°Í¨¹ý¡±)ҪôÊÇ¡°fail¡±(¼´¡°Ê§°Ü¡±)£¬¶ø²»ÊÇÌṩ¸ø³ÌÐòÔ±Ò»¸ö½âÊÍÐÔµÄÈÕÖ¾Îļþ¡£

Timely(¼°Ê±)©UÀíÏëÇé¿öÏ£¬Ó¦¸ÃÖ»ÊÇÔÚÄã±àдÉú²ú´úÂë֮ǰ±àд²âÊÔ¡£

×ñÑ­ÉÏÊöFIRSTÔ­Ôò½øÐвâÊÔÄܹ»È·±£ÄúµÄ²âÊÔÃ÷È·¶øÓÐÓ㬶ø²»ÖÂʹ֮³ÉΪÄúµÄÓ¦ÓóÌÐòÖеÄ·ÕÏ¡£

¿ªÊ¼

Ê×ÏÈ£¬Çë´ÓÍøÖ·https://koenig-media.raywenderlich.com/uploads/2016/12/Starters.zip´¦ÏÂÔØ¡¢½âѹËõ¡¢´ò¿ª²¢¹Û²ì±¾ÎÄÌṩµÄÁ½¸ö³õʼʾÀý¹¤³ÌBullsEyeºÍHalfTunes¡£

×¢Ò⣬¹¤³ÌBullsEye»ùÓÚÎÄÕÂhttps://www.raywenderlich.com/store/ios-apprenticeÖÐÌṩµÄÒ»¸öÑù±¾³ÌÐò¡£ÎÒÒѾ­°ÑÓÎÏ·Âß¼­ÌáÈ¡µ½Ò»¸öBullsEyeGameÀàÖУ¬²¢ÏàÓ¦µØÌí¼ÓÁËÁíÒ»ÖÖÓÎÏ··ç¸ñ¡£

ÔÚÓÎÏ·µÄÓÒϽÇÌṩÁËÒ»¸ö·Ö¶ÎµÄ¿ØÖÆÆ÷×é¼þ£¬¹©Óû§Ñ¡ÔñÓÎÏ··ç¸ñ©U»òÕßÊÇSlideÀàÐÍ£¬ÔÊÐíÍæ¼ÒÒÆ¶¯»¬¿é×é¼þÒÔ¾¡¿ÉÄܽӽüÄ¿±êÖµ;»òÕßÊÇTypeÀàÐÍ£¬ÔÊÐíÍæ¼Ò²Â²â»¬¿éµ½´ïµÄλÖ᣿ؼþÏàÓ¦µÄ¶¯×÷´úÂëÖл¹»á½«Óû§Ñ¡ÔñµÄÓÎÏ··ç¸ñ´æ´¢Îª¸ÃÓû§µÄĬÈÏÉèÖá£

ÁíÒ»¸öʾÀý¹¤³ÌHalfTunesÔòÀ´×ÔÓÚÎÒÃǵÄÁíÒ»¸ö½Ì³ÌNSURLSession(https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started)£¬ÏÖÒѱ»¸üе½Swift 3°æ±¾¡£Óû§¿ÉÒÔʹÓÃiTunes API²éѯ¸èÇú£¬È»ºóÏÂÔØ²¢²¥·Å¶ÔÓ¦µÄ¸èÇúƬ¶Î¡£

ÏÂÃæ£¬ÈÃÎÒÃÇÕýʽ¿ªÊ¼²âÊÔ!

XcodeÖеĵ¥Ôª²âÊÔ

´´½¨µ¥Ôª²âÊÔÄ¿±ê

XcodeÖеIJâÊÔµ¼º½Æ÷(Test Navigator)Ϊ½øÐгÌÐò²âÊÔÌṩÁË×îÈÝÒ×ʹÓõķ½Ê½;Äã¿ÉÒÔʹÓÃËü´´½¨²âÊÔÄ¿±ê²¢ÔÚÄãµÄ³ÌÐòÉÏÔËÐвâÊÔ¡£

ÏÖÔÚ£¬Çë´ò¿ª¹¤³ÌBullsEye²¢°´ÏÂ×éºÏ¼üCommand+5À´´ò¿ªËüµÄ²âÊÔµ¼º½Æ÷¡£

È»ºó£¬µã»÷×óÏ·½µÄ+°´Å¥;Ö®ºó£¬´Ó²Ëµ¥ÖÐÑ¡Ôñ¡°New Unit Test Target¡­¡±ÃüÁÈçͼËùʾ¡£

ÔÚ´Ë£¬ÇëÖ±½ÓʹÓÃĬÈϵÄÃû³ÆBullsEyeTests¡£µ±²âÊÔ°ü³öÏÖÔÚ²âÊÔµ¼º½Æ÷ÖÐʱ£¬µ¥»÷Ëü£¬´Ó¶øÔڱ༭Æ÷Öдò¿ªËü¡£Èç¹ûBullsEyeTests²»»á×Ô¶¯³öÏÖ£¬Äã¿ÉÒÔµ¥»÷ÆäËûµ¼º½Æ÷£¬È»ºóÔÙ·µ»Øµ½µ±Ç°²âÊÔµ¼º½Æ÷¼´¿É¡£

×¢Òâµ½£¬Ä£°åµ¼ÈëÁËXCTest²¢¶¨ÒåÁËXCTestCaseµÄÒ»¸ö×ÓÀàBullsEyeTests£¬Í¬Ê±ÌṩÁËsetup()·½·¨£¬tearDown()·½·¨£¬»¹ÓÐϵͳĬÈϵÄʾÀý²âÊÔ·½·¨¡£

¹éÄÉÆðÀ´£¬¹²ÓÐÈýÖÖ°ì·¨¿ÉÒÔÔËÐвâÊÔÀࣺ

1. ʹÓÃÃüÁîProduct\Test»òÕßCommand-U;Õ⽫»áÔËÐÐËùÓеIJâÊÔÀà¡£

2. ʹÓòâÊÔµ¼º½Æ÷ÖеļýÍ·ÃüÁî¡£

3. Ò²¿ÉÒÔµã»÷´úÂë×ó±ßÔµÉϵÄ×êʯ°´Å¥¡£

ÁíÍ⣬Äú»¹¿ÉÒÔͨ¹ýµ¥»÷²âÊÔµ¼º½Æ÷Öлò´úÂë×ó±ßÔµÉϵÄ×êʯ°´Å¥ÔËÐе¥¸ö²âÊÔ·½·¨¡£

½¨ÒéÄã³¢ÊÔÉÏÃæ²»Í¬µÄ·½Ê½À´ÔËÐвâÊÔ£¬´Ó¶ø¸ÐÊÜÒ»ÏÂÐèÒª¶à³¤Ê±¼äÒÔ¼°ÔËÐвâÊÔ¿´ÆðÀ´µÄÑù×Ó¡£µ±Ç°µÄÑù±¾²âÊÔ²¢²»×öÈκÎÊ£¬ËùÒÔËüÃǵÄÔËÐÐËÙ¶È»á·Ç³£¿ì!

µ±ËùÓвâÊÔ¶¼³É¹¦Ê±£¬×êʯ°´Å¥»á±äÂÌ£¬²¢ÔÚÉÏÃæÏÔʾ¶ÔºÅ±ê¼Ç¡£Äã¿ÉÒÔµ¥»÷testPerformanceExample()·½·¨×îºóÃæµÄ»ÒÉ«×êʯ°´Å¥À´´ò¿ªÐÔÄܽá¹û(Performance Result)С´°½øÐй۲죬²Î¿¼ÏÂͼ¡£

ÏÖÔÚ£¬ÎÒÃDz¢²»ÐèÒªº¯ÊýtestPerformanceExample();ËùÒÔ£¬°ÑËüɾ³ý¼´¿É¡£

ʹÓÃXCTAssert²âÊÔÄ£ÐÍ

Ê×ÏÈ£¬Äú½«Ê¹ÓÃXCTAssertÀ´²âÊÔBullsEyeÄ£Ð͵ÄÒ»¸öºËÐŦÄÜ©UÒ»¸öBullsEyeGame¶ÔÏóÄÜ·ñÕýÈ·¼ÆËã³öÒ»¸ö»ØºÏµÄµÃ·Ö?

Ϊ´Ë£¬ÇëÔÚÎļþBullsEyeTests.swiftÖнôÌù×ŵ¼ÈëÓï¾äÏ·½Ìí¼ÓÏÂÃæÕâÒ»ÐдúÂë©U

@testable import BullsEye 

ÕâÒ»ÐдúÂëʹµ¥Ôª²âÊÔÄܹ»·ÃÎʵ½BullsEyeÖеÄÀàºÍ·½·¨¡£

½ÓÏÂÀ´£¬ÇëÔÚBullsEyeTestsÀàµÄ¶¥²¿Ìí¼ÓÏÂÃæµÄÊôÐÔ£º

var gameUnderTest: BullsEyeGame! 

È»ºó£¬ÔÚsetup()·½·¨ÖÐÔÚµ÷Óó¬ÀàÓï¾äµÄÏÂÃæÆô¶¯Ò»¸öеÄBullsEyeGame¶ÔÏó£º

gameUnderTest = BullsEyeGame() 

gameUnderTest.startNewGame()

ÉÏÃæµÄ´úÂ뽫´´½¨Ò»¸öÀ༶µÄSUT(System Under Test£¬²âÊÔϵͳ)¶ÔÏó¡£ÕâÑùÒ»À´£¬²âÊÔÀàÖеÄËùÓвâÊÔ¶¼¿ÉÒÔ·ÃÎʸÃSUT¶ÔÏóµÄÊôÐԺͷ½·¨¡£

ÔÚÕâÀÄ㻹¿ÉÒÔµ÷ÓÃÓÎÏ·µÄstartNewGame·½·¨¡ª¡ª´Ë·½·¨Ö»´´½¨Ò»¸ötargetValueÖµ¡£ÄúµÄºÜ¶à²âÊÔ¶¼½«Ê¹ÓÃÕâ¸ötargetValueÖµ£¬À´²âÊÔ³ÌÐòÄܹ»ÕýÈ·¼ÆËã³öÓÎÏ·Öеĵ÷֡£

×îºó£¬ÇмÇÔÚtearDown()·½·¨ÖÐÔÚµ÷Óó¬ÀàǰÊͷŵôÄãµÄSUT¶ÔÏó©U

gameUnderTest = nil 

¡¾×¢Òâ¡¿Ò»ÖÖÖµµÃÍÆ¼öµÄ²âÊÔ×ö·¨ÊÇÔÚ·½·¨setup()Öд´½¨SUT¶ÔÏó²¢ÔÚtearDown()·½·¨ÖÐÊÍ·ÅËü£¬ÒÔÈ·±£Ã¿¸ö²âÊÔ¶¼¶ÔÓ¦Ò»¸ö³¹µ×µÄÇåÀí¡£¸ü¶àµÄÓйØÏ¸½ÚÌÖÂÛ£¬Çë²Î¿¼Jon ReidµÄÌû×Óhttp://qualitycoding.org/teardown/¡£

ÏÖÔÚ£¬ÄãÒѾ­×¼±¸ºÃ±àдÄãµÄµÚÒ»¸ö²âÊÔÁË!

ÇëʹÓÃÈçÏ´úÂëÌæ»»¹¤³ÌÖеķ½·¨testExample()£º

// XCTAssert to test model 
func testScoreIsComputed() {
// 1. given
let guess = gameUnderTest.targetValue + 5

// 2. when
_ = gameUnderTest.check(guess: guess)

// 3. then
XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}

²âÊÔ·½·¨µÄÃû³Æ×ÜÊÇÒÔtest¿ªÍ·£¬ºóÃæ¸ú×ŵÄÊǶÔËüÒª²âÊÔµÄÄÚÈݵÄ˵Ã÷¡£

Ò»¸öÍÆ¼öµÄ×ö·¨ÊǰѲâÊÔ·½·¨¸ñʽ»¯³Égiven¡¢whenºÍthenµÈ¼¸²¿·Ö©U

1. ÔÚgiven²¿·ÖÖУ¬ÉèÖÃËùÐèµÄÈκÎÖµ¡£ÔÚ´ËʾÀýÖУ¬Äú´´½¨Ò»¸ö²Â²âÖµ£¬ÒÔ±ã¿ÉÒÔÖ¸¶¨ËüÓëtargetValueÖµÇø±ð¶à´ó¡£

2. ÔÚwhen²¿·ÖÖУ¬Ö´Ðб»²âÊÔ´úÂ롪¡ªµ÷Ó÷½·¨gameUnderTest.check(_:)¡£

3. ÔÚthen²¿·ÖÖУ¬¶ÏÑÔÄãÆÚÍûµÄ½á¹û(ÔÚÏÖÔÚÇé¿öÏ£¬gameUnderTest.scoreRoundµÄÖµÊÇ100-5)£ºÈç¹û²âÊÔʧ°ÜÔò´òÓ¡¶ÔÓ¦µÄÏûÏ¢¡£

ÏÖÔÚ£¬Äã¿ÉÒÔµ¥»÷²âÊÔµ¼º½Æ÷»òÕß´úÂë×ó±ßµÄ×êʯͼ±ê°´Å¥ÔËÐвâÊÔ¡£Äã»á×¢Òâµ½Ó¦ÓóÌÐò½«½øÐй¹½¨²¢ÔËÐÐÆðÀ´£¬×îºó×êʯͼ±ê½«¸ü¸ÄΪһ¸öÂÌÉ«µÄ¶ÔºÅ±ê¼Ç!

¡¾×¢Òâ¡¿ÈôÒª²é¿´XCTestAssertionsµÄÍêÕûÁÐ±í£¬Äã¿ÉÒÔÔÚ°´ÏÂCommand¼üµÄͬʱµ¥»÷´úÂëÖеÄXCTAssertEqual´ò¿ªÎļþXCTestAssertions.h¡£´ËÍ⣬Ä㻹¿ÉÒԲο¼Æ»¹û¹Ù·½ÍøÕ¾ÌṩµÄ°´Àà±ðÌṩµÄ¶ÏÑÔÁбí

(https://developer.apple.com/library/prerelease/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/04-writing_tests.html#//apple_ref/doc/uid/TP40014132-CH4-SW35)¡£

ÁíÍ⣬ÉÏÊö²âÊÔÖеÄGiven-When-Then½á¹¹À´Ô´ÓÚÐÐΪÇý¶¯²âÊÔ(Behavior Driven Development£¬¼ò³ÆBDD)ÖеÄÒ×ÓÚÀí½âµÄÐÐÒµÊõÓï¡£Æäʵ£¬Ä㻹¿ÉÒÔʹÓÃÁíÍâһЩÃüÃûϵͳ£¬ÀýÈçArrange-Act-AssertºÍAssemble-Activate-Assert£¬µÈµÈ¡£

µ÷ÊÔÒ»¸ö²âÊÔ

ÔÚBullsEyeGame¹¤³ÌÖУ¬ÎÒ¹ÊÒâ·ÅÖÃÁËÒ»¸ö´íÎó¡£ÏÖÔÚ£¬ÎÒÃǽøÐвâÊÔ£¬ÒÔ±ãÕÒµ½Õâ¸ö´íÎó¡£ÎªÁ˹۲ì´Ë´íÎóµ¼ÖµÄÎÊÌ⣬Çë°ÑtestScoreIsComputedÖØÐÂÃüÃûΪtestScoreIsComputedWhenGuessGTTarget£¬È»ºó¸´ÖÆ¡¢Õ³Ìù²¢±à¼­Ëü£¬´Ó¶ø´´½¨ÁíÒ»¸ö·½·¨testScoreIsComputedWhenGuessLTTarget¡£

ÔڸòâÊÔÖУ¬ÔÚgiven²¿·Ö°ÑtargetValue¼õÈ¥5£¬ÆäËû±£³Ö²»±ä¡£Ïê¼ûÏÂÁдúÂ룺

func testScoreIsComputedWhenGuessLTTarget() { 
// 1. given
let guess = gameUnderTest.targetValue - 5

// 2. when
_ = gameUnderTest.check(guess: guess)

// 3. then
XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}

×¢Òâµ½£º²Â²âÖµºÍtargetValueÖµÖ®¼äµÄÇø±ðÈÔÈ»ÊÇ5£¬Òò´Ë·ÖÊýÓ¦ÈÔΪ95¡£

Ôڶϵ㵼º½Æ÷ÖУ¬Ìí¼ÓÒ»¸ö²âÊÔʧ°Ü(Test Failure)¶Ïµã;µ±Ò»¸ö²âÊÔ·½·¨·¢³öÒ»¸öʧ°ÜµÄ¶ÏÑÔʱÕ⽫ֹͣ²âÊÔÔËÐС£

ÏÖÔÚÔËÐÐÄãµÄ²âÊÔ£ºËüÓ¦¸ÃÔÚXCTAssertEqualÒ»ÐÐÍ£Ö¹£¬²¢³öʾһ¸ö²âÊÔ´íÎó¡£

È»ºó£¬Äã¿ÉÒÔÔÚµ÷ÊÔ¿ØÖÆÌ¨ÉϹ۲ìgameUnderTestºÍguessµÄÊä³ö½á¹û£º

ÄãÓ¦¸Ã×¢Òâµ½£ºguessµÄÖµÊÇ-5£¬µ«scoreRoundµÄÖµÊÇ105£¬¶ø²»ÊÇ95!

ΪÁ˽øÒ»²½·ÖÎö£¬Äã¿ÉÒÔʹÓÃͨ³£µÄµ÷ÊÔ¹ý³Ì©UÔÚwhenÓï¾äÉÏÉèÖÃÒ»¸ö¶Ïµã£¬Ò²ÔÚBullsEyeGame.swiftÎļþÉÏÉèÖÃÒ»¸ö¶Ïµã¡ª¡ª¼´ÔÚÆäÖеķ½·¨check(_:)ÉÏÉèÖá£È»ºó£¬ÔÙ´ÎÔËÐвâÊÔ£¬²¢ÒÔÖð¹ý³Ìµ÷ÊÔ·½Ê½(¼´step-over)µ÷ÊÔletÓï¾äÀ´¼ì²éÓ¦ÓóÌÐòÖеIJ»Í¬Öµ¡£

ÏÖÔÚµÄÎÊÌâÊÇ£¬²îÖµÊÇÒ»¸ö¸ºÊý;ËùÒÔ£¬µÃ·ÖÊÇ100-(-5)¡£½â¾ö·½·¨ÊÇʹÓòîÒìµÄ¾ø¶ÔÖµ¼´¿É¡£Îª´Ë£¬ÔÚ·½·¨check(_:)ÖÐÈ¡ÏûÕýÈ·´úÂëÇ°ÃæµÄ×¢ÊÍ£¬²¢É¾³ý²»ÕýÈ·µÄ´úÂë¼´¿É¡£

ɾ³ýÉÏÃæÉèÖõÄÁ½¸ö¶Ïµã²¢ÔÙÒ»´ÎÔËÐвâÊÔ£¬ÒÔÈ·ÈÏÉÏÃæ´úÂëÐÐÏÖÔÚÒÑ˳Àûͨ¹ý¡£

ʹÓÃXCTestExpectation²âÊÔÒì²½²Ù×÷

µ½Ä¿Ç°ÎªÖ¹£¬ÄãÒѾ­Ñ§»áÁËÈçºÎ²âÊÔÄ£Ðͺ͵÷ÊÔ²âÊÔʧ°Ü¡£½ÓÏÂÀ´£¬ÈÃÎÒÃǼÌÐøÑ§Ï°ÈçºÎʹÓÃXCTestExpectationÀ´²âÊÔÍøÂçÏà¹ØµÄ²Ù×÷¡£

Ê×ÏÈ£¬Çë´ò¿ªHalfTunesÏîÄ¿¡£Äã»á×¢Òâµ½£¬ËüʹÓÃURLSessionÀ´²éѯiTunes APIºÍÏÂÔØ¸èÇúÑù±¾¡£¼ÙÉèÄúÏëÐÞ¸ÄËü£¬ÒÔ±ãʹÓÃAlamoFire½øÐÐÂç²Ù×÷¡£ÎªÁ˲鿴ÊÇ·ñ³öÏÖÈκÎÖжÏÇé¿ö£¬ÄúÓ¦ÎªÍøÂç²Ù×÷±àд²âÊÔ£¬²¢ÔÚ¸ü¸Ä´úÂë֮ǰºÍÖ®ºóÔËÐÐËüÃÇ¡£

URLSession·½·¨ÊÇÒì²½Ö´ÐеĩUËüÃÇ»áÂíÉÏ·µ»Ø£¬µ«Ö»ÓÐÔËÐÐÒ»¶Îʱ¼äºó²ÅÕæÕýÍê³É¡£ÎªÁ˲âÊÔÒì²½·½·¨£¬ÄãӦʹÓÃXCTestExpectationʹÄãµÄ²âÊԵȴýÒì²½²Ù×÷Íê³É¡£

ÖµµÃ×¢ÒâµÄÊÇ£¬Òì²½²âÊÔͨ³£ºÜÂý£¬ËùÒÔÄãÓ¦¸Ã°ÑËüÃÇÓëÄãÁíÍâµÄһЩÔËÐÐËٶȸü¿ìµÄµ¥Ôª²âÊÔ·Ö¿ª¡£

´Ó²Ëµ¥¡°+¡±ÏÂÑ¡Ôñ²¢ÔËÐÐÃüÁî¡°New Unit Test Target¡­¡±£¬È»ºó°ÑÄ¿±êÃüÃûΪHalfTunesSlowTests¡£È»ºó£¬ÔÚimportÓï¾äµÄÏÂÃæµ¼ÈëHalfTunes³ÌÐò£º

@testable import HalfTunes 

ÔÚ´ËÀàÖеÄËùÓвâÊÔ¶¼½«Ê¹ÓÃĬÈϻỰ°ÑÇëÇó·¢Ë͵½Æ»¹û¹«Ë¾µÄ·þÎñÆ÷¡£ËùÒÔ£¬ÎÒÃÇÔÚ·½·¨setup()ÖÐÉùÃ÷²¢´´½¨Ò»¸ösessionUnderTest¶ÔÏó£¬È»ºóÔÚ·½·¨tearDown()ÖÐÊÍ·ÅËü£º

var sessionUnderTest: URLSession! 
override func setUp() {
super.setUp()
sessionUnderTest = URLSession(configuration: URLSessionConfiguration.default)
}
override func tearDown() {
sessionUnderTest = nil
super.tearDown()
}

½ÓÏÂÀ´£¬Ê¹ÓÃTestExample()º¯ÊýÀ´Ìæ»»ÄúµÄÒì²½²âÊÔ©U

//Òì²½²âÊÔʱ£º³É¹¦²âÊԺܿ죬ʧ°Ü²âÊÔÈ´±È½ÏÂý 
func testValidCallToiTunesGetsHTTPStatusCode200() {
// given
let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
// 1
let promise = expectation(description: "Status code: 200")

// when
let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
// then
if let error = error {
XCTFail("Error: \(error.localizedDescription)")
return
} else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
if statusCode == 200 {
// 2
promise.fulfill()
} else {
XCTFail("Status code: \(statusCode)")
}
}
}
dataTask.resume()
// 3
waitForExpectations(timeout: 5, handler: nil)
}

ÉÏÃæÕâ¸ö²âÊÔµÄÄ¿µÄÊǼì²é·¢Ë͵½iTunesµÄÓÐЧµÄ²éѯÊÇ·ñÄܹ»·µ»Ø×´Ì¬Âë200¡£ÏÔÈ»£¬ÆäÖд󲿷ִúÂëÓëÄãÔÚÉÏÃæÓ¦ÓóÌÐòÖÐËùдµÄÒ»Ñù£¬Ö»ÊÇÔö¼ÓÁËÈçϼ¸ÐЩU

1.expectation(_:)·µ»ØÒ»¸öXCTestExpectation¶ÔÏó;´Ë¶ÔÏó´æ´¢ÔÚ±äÁ¿promiseÖС£´Ë¶ÔÏóµÄÆäËû³£ÓÃÃû×ÖÊÇexpectationºÍfuture¡£ÁíÍ⣬description²ÎÊýÃèÊöÁËÄãÆÚÍû·¢ÉúµÄÊÂÇé¡£

2.ΪÁËÆ¥Åädescription²ÎÊý£¬ÄúÐèÒªÔÚÒì²½·½·¨µÄÍê³É´¦Àí³ÌÐòµÄ³É¹¦Ìõ¼þ±Õ°üÖе÷ÓÃpromise.fulfill()¡£

3.waitForExpectations(_:handler:)µÄ×÷ÓÃÊDZ£³ÖËùÓвâÊÔÔÚÔËÐÐÖУ¬Ö±µ½ËùÓÐµÄÆÚÍûµÃÒÔʵÏÖ£¬»òÕßtimeoutÖµÖ¸¶¨µÄʱ¼ä¼ä¸ô½áÊø¡ª¡ªÎÞÂÛÁ½ÕßÄÄÒ»ÖÖÔç·¢Éú¶¼ÐС£

ÏÖÔÚ£¬ÔÙÀ´ÔËÐиòâÊÔ¡£Èç¹ûÄãÒѾ­Á¬½Óµ½»¥ÁªÍø£¬Ôòµ±Ó¦ÓóÌÐòÔÚÄ£ÄâÆ÷ÖмÓÔØºó³É¹¦²âÊÔ´óÔ¼»¨·ÑÒ»ÃëÖÓʱ¼ä¡£

ʹ²âÊÔʧ°Ü¸ü¿ìһЩ

²âÊÔʧ°Ü»áµ¼Ö²»ÉÙÎÊÌ⣬µ«Ëüδ±Ø»¨·ÑºÜ¶àʱ¼ä¡£ÏÖÔÚ£¬ÎÒÃÇÀ´½â¾öÈçºÎ¿ìËÙÈ·¶¨ÊÇ·ñÄúµÄ²âÊÔʧ°ÜµÄÎÊÌâ¡£

ΪÁËÐÞ¸ÄÒ»ÏÂÄúµÄ²âÊÔ£¬´Ó¶øµ¼ÖÂÒì²½²Ù×÷ʱʧ°Ü£¬ÄãÖ»ÐèÒª´ÓÏÂÃæµÄURLÖÐɾ³ý¡°itunes¡±Ò»´ÊºóÃæµÄs×Öĸ¼´¿É£º

let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba") 

ÔËÐÐÉÏÊö²âÊÔʱ©UËü»áʧ°Ü£¬¶øÇҴ˲âÊԻỨ·ÑËùÓÐÖ¸¶¨µÄ³¬Ê±¼ä¸ôʱ¼ä!ÕâÊÇÒòΪËüµÄÆÚÍûÊÇÇëÇó³É¹¦¡ª¡ªÕýÊÇÔÚÕâ¸öλÖõ÷ÓÃÁËpromise.fulfill()·½·¨¡£¼ÈÈ»ÇëÇóʧ°Ü£¬ÄÇô²âÊÔ½öµ±ÔÚ³¬¹ýÖ¸¶¨Ê±ÏÞʱ²Å½áÊø¡£

Äã¿ÉÒÔʹÕâ¸ö²âÊÔʧ°Ü¸ü¿ìһЩ¡ª¡ªÕâֻҪͨ¹ý¸Ä±äËüµÄÆÚÍûÖµ¼´¿É´ïµ½©U²»ÊǵȴýÇëÇó³É¹¦£¬¶øÖ»ÐèÒªµÈµ½Òì²½·½·¨µÄÍê³É´¦Àí³ÌÐò´¥·¢¼´¿É¡£Ö»ÒªÓ¦ÓóÌÐò½ÓÊÕµ½À´×Ô·þÎñÆ÷¶ËµÄÏìÓ¦(»òÕßÊdzɹ¦»òÕßÊÇʧ°Ü)ÕâÖÖÇé¿ö¾Í»á·¢Éú;µ«ÊÇ£¬ÕâµÄÈ··ûºÏÔ¤ÆÚ½á¹û¡£È»ºó£¬ÄúµÄ²âÊÔ¿ÉÒÔ¼ì²éÇëÇóÊÇ·ñ³É¹¦¡£

ΪÁ˲鿴ÕâÊÇÈçºÎ¹¤×÷µÄ£¬ÄúÒª´´½¨Ò»¸öеIJâÊÔ¡£Ê×ÏÈ£¬ÐÞ¸´´Ë²âÊÔ¡ª¡ªÕâ¿ÉÒÔͨ¹ý³·ÏûÉÏÃæµÄurl¸ü¸Ä²Ù×÷ÇáËÉÍê³É£¬È»ºó½«ÏÂÃæµÄ²âÊÔÌí¼Óµ½ÄúµÄÀàÖЩU

// Asynchronous test: faster fail 
func testCallToiTunesCompletes() {
// given
let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
// 1
let promise = expectation(description: "Completion handler invoked")
var statusCode: Int?
var responseError: Error?

// when
let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
statusCode = (response as? HTTPURLResponse)?.statusCode
responseError = error
// 2
promise.fulfill()
}
dataTask.resume()
// 3
waitForExpectations(timeout: 5, handler: nil)

// then
XCTAssertNil(responseError)
XCTAssertEqual(statusCode, 200)
}

ÉÏÃæ´úÂëÖÐ×î¹Ø¼üµÄÒ»µãÊÇ£¬Ö»ÐèÊäÈëÍê³É´¦Àí³ÌÐòʵÏֵįÚÍû¡ª¡ªÕâÐèÒª´óÔ¼Ò»ÃëÖÓ¼´»á·¢Éú¡£Èç¹ûÇëÇóʧ°Ü£¬ÄÇô¶ÏÑÔÒ²»áʧ°Ü¡£

ÏÖÔÚÔÙÀ´ÔËÐÐÉÏÃæµÄ²âÊÔ©UËüÏÖÔÚ´óÔ¼ÐèÒªÒ»ÃëÖÓ¼´»áʧ°Ü;ËüµÄʧ°ÜÊÇÒòΪÇëÇóʧ°ÜÁË£¬¶ø²»ÊÇÒòΪ²âÊÔÔËÐг¬Ê±¡£

ÐÞ¸´ÉÏÃæµÄurl£¬È»ºóÔÙÒ»´ÎÔËÐвâÊÔ£¬ÒÔÈ·ÈÏËüÏÖÔÚÄܹ»³É¹¦Í¨¹ý²âÊÔ¡£

αÔì¶ÔÏóºÍ½»»¥

Òì²½²âÊÔÄܹ»¸øÄãÐÅÐÄ¡ª¡ªÄãµÄ´úÂë»áΪһ¸öÒì²½APIÌṩÕýÈ·µÄÊäÈë¡£Äã¿ÉÄÜÒ²Ïë²âÊÔÄúµÄ´úÂëÄܹ»Õý³£¹¤×÷¡ª¡ªµ±Ëü´ÓURLSession½ÓÊÕÊäÈëʱ£¬»òµ±ËüÕýÈ·¸üÐÂÁËUserDefaults»òÕßCloudKitÊý¾Ý¿âʱ¡£

´ó¶àÊýÓ¦ÓóÌÐò¶¼»áÓëϵͳ»ò¿â¶ÔÏó(Äã²»ÄÜ¿ØÖÆÕâЩ¶ÔÏó)½øÐн»»¥£¬¶øÓëÕâЩ¶ÔÏóµÄ½»»¥²âÊԺܿÉÄÜÊǼ«Æä»ºÂýµÄ£¬¶øÇÒ²»¿ÉÖØ¸´µÄ¡ª¡ªÕâÕýÎ¥·´ÁËÎÄÕ¿ªÊ¼Ê±FIRSTÔ­ÔòÖеÄÁ½Ìõ¡£Ïà·´£¬Äã¿ÉÒÔαÔìÕâЩ½»»¥¡ª¡ªÍ¨¹ý´Ó´úÀí(stub)ÖлñÈ¡ÊäÈë»ò¸üÐÂÄ£Äâ¶ÔÏó(Mock Object)À´ÊµÏÖ¡£

µ±ÄúµÄ´úÂëÒÀÀµÓÚÒ»¸öϵͳ»ò¿âÖеĶÔÏóʱ£¬Í¨¹ýÉÏÃæÎ±ÔìµÄ°ì·¨¿ÉÒÔ´´½¨Ò»¸ö¼ÙµÄ¶ÔÏóÀ´ÊµÏÖÄÇÒ»²¿·Ö¹¦Äܲ¢°ÑÕâÖÖαÔì×¢Èëµ½ÄúµÄ´úÂëÖС£ÇǶ÷¡¤ÀïµÂµÄÒÀÀµÐÔ×¢Èë¼¼ÊõÎÄÕÂ(https://www.objc.io/issues/15-testing/dependency-injection/)ÖоͽéÉÜÁ˺ü¸ÖÖ·½·¨À´´ïµ½ÕâһĿµÄ¡£

´Ó´úÀí(stub)ÖÐαÔìÊäÈë

ÔÚ±¾½ÚÖеIJâÊÔÖУ¬Ä㽫Ҫ¼ì²éÓ¦ÓóÌÐòµÄupdateSearchResults(_:)·½·¨Äܹ»ÕýÈ·½âÎöÓɻỰÏÂÔØµÄÊý¾Ý¡ª¡ªÍ¨¹ý¼ì²éÊôÐÔsearchResults.countµÄÖµÊÇÕýÈ·µÄÀ´ÊµÏÖ¡£SUTÊÇÊÓͼ¿ØÖÆÆ÷;ÄãҪʹÓôúÀí(stub)¼¼ÊõÀ´Î±×°Ò»¸ö»á»°ºÍһЩԤÏÈÏÂÔØµÄÊý¾Ý¡£

Ϊ´Ë£¬´Ó¡°+¡±²Ëµ¥ÏÂÑ¡ÔñÃüÁî¡°New Unit Test Target¡­¡±²¢ÃüÃûËüΪHalfTunesFakeTests¡£È»ºó£¬ÔÚimportÓï¾äµÄÏÂÃæµ¼ÈëHalfTunes³ÌÐò£º

@testable import HalfTunes 

½ÓÏÂÀ´£¬ÉùÃ÷SUT£¬²¢ÔÚsetup()·½·¨Öд´½¨Ëü£¬ÇÒÔÚtearDown()·½·¨ÖжÔÖ®½øÐÐÊÍ·Å£º

var controllerUnderTest: SearchViewController! 

override func setUp() {
super.setUp()
controllerUnderTest = UIStoryboard(name: "Main",
bundle: nil).instantiateInitialViewController() as! SearchViewController!
}

override func tearDown() {
controllerUnderTest = nil
super.tearDown()
}

¡¾×¢¡¿SUT(±»²âϵͳ)ÊÇÊÓͼ¿ØÖÆÆ÷£¬ÒòΪHalfTunes¹¤³ÌÖÐÓµÓдóÁ¿µÄÊÓͼ¿ØÖÆÆ÷ÎÊÌ⡪¡ªËùÓеŤ×÷¶¼ÊÇÔÚÎļþsearchviewcontroller.swiftÖÐÍê³ÉµÄ¡£¡°½«ÍøÂç´úÂëÒÆ¶¯µ½µ¥¶ÀµÄÄ£¿é¡±(Ïê¼ûÎÄÕÂhttp://williamboles.me/networking-with-nsoperation-as-your-wingman/)½«»á¼õÉÙÕâÒ»ÎÊÌ⣬¶øÇÒҲʹ²âÊÔ¸üΪÈÝÒס£

½ÓÏÂÀ´£¬Äú½«ÐèҪһЩÑù±¾JSONÊý¾Ý£¬¹©ÄúµÄαÔìµÄ»á»°Ìṩ¸øÄãµÄ²âÊÔʹÓá£Ö»ÐèÒª×öÒ»ÉÙ²¿·Ö¹¤×÷¼´¿É;Òò´Ë£¬ÇëÏÞÖÆÒ»ÏÂÄúµÄÀ´×ÔiTunesµÄÏÂÔØ½á¹û¡ª¡ªÔÚURL×Ö·û´®µÄºóÃæÌí¼ÓÒ»¸öÏÞÖÆ´®&limit=3£º

https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3

¸´ÖÆ´ËURL²¢°ÑËüÕ³Ìùµ½ä¯ÀÀÆ÷ÖС£Õ⽫ÏÂÔØÒ»¸öÃûΪ1.txt»òÀàËÆµÄÎļþ¡£Äã¿ÉÒÔÔ¤ÀÀÒ»ÏÂËü£¬ÒÔ±ãÈ·ÈÏÕâÊÇÒ»¸öJSON¸ñʽµÄÎļþ£¬È»ºóÖØÃüÃûËüΪabbaData.json£¬²¢°Ñ¸ÃÎļþÌí¼Óµ½HalfTunesFakeTests×éÖС£

HalfTunesÏîÄ¿°üº¬ÁËÖ§³ÖÎļþDHURLSessionMock.swift¡£Õâ¸öÎļþÖж¨ÒåÁËÒ»¸ö¼òµ¥µÄЭÒ顪¡ªDHURLSession£¬ÆäÌṩµÄ·½·¨(´úÀí)ÓÃÓÚʹÓÃÒ»¸öURL»òURLRequestÀ´´´½¨Ò»¸öÊý¾ÝÈÎÎñ¡£Ëü»¹¶¨ÒåÁË·ûºÏ¸ÃЭÒéµÄURLSessionMock¶ÔÏ󣬸öÔÏóÖÐÌṩµÄ³õʼ»¯Æ÷¿ÉÒÔÈÃÄãʹÓÃÄãÑ¡ÔñµÄÊý¾Ý¡¢ÏìÓ¦ºÍÎó²îµÈÀ´´´ÔìÒ»¸öÄ£ÄâURLSession¶ÔÏó¡£

ÏÖÔÚ£¬ÎÒÃÇÀ´¹¹½¨Î±ÔìµÄÊý¾ÝºÍÏìÓ¦£¬²¢´´½¨Î±ÔìµÄ»á»°¶ÔÏó;ÕâЩ¶¼ÊµÏÖÓÚ·½·¨setup()ÖУ¬ÏàÓ¦µÄ´úÂëλÓÚ´´½¨SUT¶ÔÏóµÄÓï¾äÖ®ºó£º

let testBundle = Bundle(for: type(of: self)) 
let path = testBundle.path(forResource: "abbaData", ofType: "json")
let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped)

let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
let urlResponse = HTTPURLResponse(url: url!, statusCode: 200, httpVersion: nil, headerFields: nil)

let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)
At the end of setup(), inject the fake session into the app as a property of the SUT:

controllerUnderTest.defaultSession = sessionMock

¡¾×¢Òâ¡¿Äú½«Ö±½ÓÔÚÄúµÄ²âÊÔÖÐʹÓÃαÔìµÄ»á»°£¬µ«ÊÇÕ⽫ÏòÄãչʾÈçºÎ×¢ÈëÕâÖÖαÔìµÄ»á»°;ÕâÑùÒ»À´£¬Äã½øÒ»²½µÄ²âÊÔ¿ÉÒÔµ÷ÓÃʹÓÃÊÓͼ¿ØÖÆÆ÷defaultSessionÊôÐÔµÄSUT·½·¨¡£

ÏÖÔÚ£¬Äú¿ÉÒÔ±àд²âÊÔÀ´¼ì²éÊÇ·ñµ÷ÓÃupdateSearchResults(_:)·½·¨Äܹ»½âÎöαÔìµÄÊý¾Ý¡£Îª´Ë£¬Çë°ÑTestExample()·½·¨Ì滻ΪÒÔÏÂÄÚÈÝ©U

//ʹÓÃDHURLSessionЭÒéºÍ´úÀíαÔìURLSession 
func test_UpdateSearchResults_ParsesData() {
// given
let promise = expectation(description: "Status code: 200")

// when
XCTAssertEqual(controllerUnderTest?.searchResults.count, 0, "searchResults should be empty before the data task runs")
let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
let dataTask = controllerUnderTest?.defaultSession.dataTask(with: url!) {
data, response, error in
// if HTTP request is successful, call updateSearchResults(_:) which parses the response data into Tracks
if let error = error {
print(error.localizedDescription)
} else if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode == 200 {
promise.fulfill()
self.controllerUnderTest?.updateSearchResults(data)
}
}
}
dataTask?.resume()
waitForExpectations(timeout: 5, handler: nil)

// then
XCTAssertEqual(controllerUnderTest?.searchResults.count, 3, "Didn't parse 3 items from fake response")
}

×¢Ò⣬ÄãÈÔȻҪÒÔÒì²½·½Ê½À´±àдÕâ¸ö²âÊÔ£¬ÒòΪ´úÀí(stub)¼Ù×°×Ô¼ºÊÇÒ»¸öÒì²½µÄ·½·¨¡£

ÉÏÃæ´úÂëÖУ¬when¶ÏÑÔµÄ×÷ÓÃÊÇ£ºÔÚÊý¾ÝÈÎÎñÔËÐÐ֮ǰsearchResultsµÄÖµÓ¦µ±Êǿյġª¡ªÕâÓ¦¸ÃÊÇÕæÊµÇé¿ö£¬ÒòΪÄúÔÚsetup()·½·¨Öд´½¨ÁËÒ»¸öȫеÄSUT¡£

αÔìµÄÊý¾Ý°üº¬ÁËÌṩ¸øÈý¸ö¸ú×Ù(Track)¶ÔÏóʹÓõÄJSONÊý¾Ý;ËùÒÔ£¬then¶ÏÑÔµÄ×÷ÓÃÊÇ£ºÊÓͼ¿ØÖÆÆ÷µÄsearchResultsÊý×éÓ¦µ±°üº¬ÈýÏî¡£

ÔÙ´ÎÔËÐиòâÊÔ¡£Õâ´ÎÓ¦¸Ã³É¹¦£¬¶øÇÒËٶȺܿ죬ÒòΪ²»´æÔÚÈκÎÕæÊµµÄÍøÂçÁ¬½Ó!

αÔì¶ÔÄ£Äâ¶ÔÏóµÄ¸üÐÂ

ÒÔǰµÄ²âÊÔʹÓôúÀí´Ó¼Ù¶ÔÏóÌṩÊäÈë¡£½ÓÏÂÀ´£¬Äã¿ÉÒÔʹÓÃÒ»¸öÄ£Äâ¶ÔÏóÀ´²âÊÔÄãµÄ´úÂë¿ÉÒÔÕýÈ·¸üÐÂUserDefaults¡£

ÖØÐ´ò¿ªBullsEyeÏîÄ¿¡£×¢Òâµ½£¬¸ÃÓ¦ÓóÌÐòÌṩÁËÁ½ÖÖÓÎÏ··ç¸ñ£ºÓû§¿ÉÒÔÑ¡ÔñÒÆ¶¯»¬¿éÀ´Æ¥ÅäÄ¿±êÖµ»ò´Ó»¬¿éλÖò²âÄ¿±êÖµ¡£½èÖúÓÚ½çÃæÓÒϽǵķֶοØÖÆ¿ª¹Ø¿ÉÒÔÇл»ÓÎÏ··ç¸ñ²¢¸üÐÂÓû§Ä¬ÈϵÄÓÎÏ··ç¸ñ¡£

ÄãÒª±àдµÄÏÂÒ»¸ö²âÊÔ½«¼ì²éÓ¦ÓóÌÐòÄܹ»ÕýÈ·µØ¸üÐÂÓû§Ä¬ÈϵÄÓÎÏ··ç¸ñÊý¾Ý¡£

ÔÚ²âÊÔµ¼º½Æ÷ÖУ¬µã»÷ÃüÁî¡°New Unit Test Target¡­¡±£¬²¢ÃüÃûΪBullsEyeMockTests¡£È»ºó£¬ÔÚµ¼ÈëÓï¾äÏÂÃæÌí¼ÓÒÔÏÂÄÚÈÝ£º

@testable import BullsEye 

class MockUserDefaults: UserDefaults {
var gameStyleChanged = 0
override func set(_ value: Int, forKey defaultName: String) {
if defaultName == "gameStyle" {
gameStyleChanged += 1
}
}
}

×¢Òâµ½£¬ÉÏÃæµÄMockUserDefaultsÀàÖØÔØÁËset(_:forKey:)·½·¨ÒÔ±ã°ÑgameStyleChanged±êÖ¾µÄÖµ¼Ó1¡£Í¨³£Äã»á¿´µ½ÀàËÆµÄ²âÊÔÖÐÊÇÉèÖÃÒ»¸ö²¼¶û±äÁ¿£¬µ«ÊÇÔÚ´ËÎÒÃÇʹÓÃÒ»¸öÕûÊýÖµ¼Ó1£¬Õâ¿ÉÒÔ½øÒ»²½Ôö¼ÓÄãµÄÁé»î¿ØÖÆ¡ª¡ªÀýÈçÄãµÄ²âÊÔ¿ÉÒÔ¼ì²é¸Ã·½·¨½ö±»ÕýÈ·µØµ÷ÓÃÒ»´Î¡£

ÔÚBullsEyeMockTestsÀàÖÐÉùÃ÷SUT¶ÔÏóºÍÄ£Äâ¶ÔÏó£º

var controllerUnderTest: ViewController! 
var mockUserDefaults: MockUserDefaults!

ÔÚ·½·¨setup()ÖУ¬´´½¨SUT¶ÔÏóºÍÄ£Äâ¶ÔÏó£¬È»ºó°Ñ´ËÄ£Äâ¶ÔÏó×¢ÈëΪ¸ÃSUTµÄÒ»¸öÊôÐÔ£º

controllerUnderTest = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController! 
mockUserDefaults = MockUserDefaults(suiteName: "testing")!
controllerUnderTest.defaults = mockUserDefaults
Release the SUT and the mock object in tearDown():
controllerUnderTest = nil
mockUserDefaults = nil
Replace testExample() with this:
// Mock to test interaction with UserDefaults
func testGameStyleCanBeChanged() {
// given
let segmentedControl = UISegmentedControl()

// when
XCTAssertEqual(mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions")
segmentedControl.addTarget(controllerUnderTest,
action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged)
segmentedControl.sendActions(for: .valueChanged)

// then
XCTAssertEqual(mockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed")
}

ÉÏÊö´úÂëÖеÄwhen¶ÏÑÔµÄ×÷ÓÃÊÇ£ºgameStyleChanged±êÖ¾µÄֵΪ0¡ª¡ªÔÚ²âÊÔ·½·¨´¥·¢·Ö¶Î¿ØÖÆ¿ª¹ØÖ®Ç°¡£Òò´Ë£¬Èç¹ûthen¶ÏÑÔÒ²ÎªÕæ£¬ÄÇô½«Òâζ×Å·½·¨set(_:forKey:)½ö±»ÕýÈ·µØµ÷ÓÃÒ»´Î¡£

ÏÖÔÚÔÙ´ÎÔËÐвâÊÔ;Ó¦µ±¿ÉÒԳɹ¦¡£

ÔÚXcodeÖнøÐÐUI²âÊÔ

Xcode 7ÖÐÒýÈëÁ˶ÔUI²âÊÔµÄÖ§³Ö£¬Ê¹Äú¿ÉÒÔͨ¹ý¼Ç¼ÓëUIµÄ½»»¥À´´´½¨UI²âÊÔ¡£UI²âÊԵŤ×÷·½Ê½ÊÇ£ºÍ¨¹ý²éѯÀ´²éÕÒÒ»¸öÓ¦ÓóÌÐòµÄUI¶ÔÏ󣬽ø¶øºÏ³Éʼþ£¬È»ºó½«ÕâЩʼþ·¢Ë͸øÕâЩ¶ÔÏ󡣯äÌṩµÄAPIʹÄú¿ÉÒÔ¼ì²éÒ»¸öÓû§½çÃæ¶ÔÏóµÄÊôÐÔºÍ״̬£¬ÒÔ±ã°ÑËüÃÇÓëÔ¤ÆÚµÄ״̬½øÐбȽϡ£

ÏÖÔÚ£¬ÈÃÎÒÃÇÔÚBullsEyeÏîÄ¿µÄ²âÊÔµ¼º½Æ÷ÖÐÌí¼ÓÒ»¸öеÄUI²âÊÔÄ¿±ê¡£È·±£Òª±»²âÊÔµÄÄ¿±êÊÇBullsEye£¬È»ºó½ÓÊÜĬÈÏÃû³ÆBullsEyeUITests¡£

È»ºó£¬ÔÚBullsEyeUITestsÀàµÄ¶¥²¿Ìí¼ÓÈçÏÂÊôÐÔ©U

var app: XCUIApplication! 

ÔÚ·½·¨setup()ÖУ¬ÓÃÒÔÏ´úÂëÌæ»»XCUIApplication().launch()Óï¾ä©U

app = XCUIApplication() 

app.launch()

°ÑtestExample()µÄÃû×Ö¸ü¸ÄΪtestGameStyleSwitch()¡£

È»ºó£¬ÔÚtestGameStyleSwitch()Öа´Ï»سµ¼ü´´½¨Ò»¸öеĿÕÐУ¬²¢µã»÷±à¼­Æ÷´°¿Úµ×²¿µÄºìÉ«µÄRecord°´Å¥£¬ÈçͼËùʾ¡£

µ±Ó¦ÓóÌÐò³öÏÖÔÚÄ£ÄâÆ÷ÖÐʱ£¬µã»÷¿ØÖÆÓÎÏ··ç¸ñ¿ª¹ØµÄ»¬¶¯¿é¼°¶¥²¿±êÇ©¡£È»ºó£¬µ¥»÷XcodeÖеÄRecord°´Å¥¼´¿ÉÍ£Ö¹Â¼ÖÆ¡£

ÏÖÔÚ£¬ÄãÔÚ·½·¨testGameStyleSwitch()ÖÐÓµÓÐÒÔÏÂÈýÐдúÂë©U

let app = XCUIApplication() 

app.buttons["Slide"].tap()

app.staticTexts["Get as close as you can to: "].tap()

Èç¹û»¹ÓÐÆäËûµÄÓï¾ä£¬Ôòɾ³ýËüÃÇ¡£

µÚÒ»ÐдúÂëµÄ×÷ÓÃÊǸ´ÖÆÄãÔÚsetup()·½·¨Öд´½¨µÄÊôÐÔ;ÒòΪÄ㻹²»ÐèÒªµã»÷Èκζ«Î÷£¬ËùÒÔÒ²°ÑÕâµÚÒ»ÐÐɾ³ý£¬»¹ÒªÉ¾³ýµÚ2ÐÐÓëµÚ3ÐÐĩβµÄ¡°.tap()¡±¡£´ò¿ª["Slide"]ÁÚ½üµÄС²Ëµ¥²¢Ñ¡ÔñsegmentedControls.buttons["Slide"]¡£

ÓÚÊÇ£¬ÄãÓÐÁËÈçϵĴúÂ룺

app.segmentedControls.buttons["Slide"] 

app.staticTexts["Get as close as you can to: "]

½øÒ»²½ÐÞ¸ÄÉÏÊö´úÂ룬ÒԱ㴴½¨²âÊÔµÄgiven²¿·Ö£º

// given 

let slideButton = app.segmentedControls.buttons["Slide"]

let typeButton = app.segmentedControls.buttons["Type"]

let slideLabel = app.staticTexts["Get as close as you can to: "]

let typeLabel = app.staticTexts["Guess where the slider is: "]

ÏÖÔÚ£¬ÄãÓÐÁËÁ½¸ö°´Å¥ºÍÁ½¸ö¿ÉÄܵĶ¥²¿±êÇ©µÄÃû³Æ£¬ÔÙÌí¼ÓÒÔÏÂÄÚÈÝ©U

// then 

if slideButton.isSelected {

XCTAssertTrue(slideLabel.exists)

XCTAssertFalse(typeLabel.exists)

typeButton.tap()

XCTAssertTrue(typeLabel.exists)

XCTAssertFalse(slideLabel.exists)

} else if typeButton.isSelected {

XCTAssertTrue(typeLabel.exists)

XCTAssertFalse(slideLabel.exists)

slideButton.tap()

XCTAssertTrue(slideLabel.exists)

XCTAssertFalse(typeLabel.exists)

}

Õâ¶Î´úÂ뽫»á¼ì²âµ±Ñ¡ÖлòÕßµã»÷ÿ¸ö°´Å¥Ê±ÊÇ·ñ´æÔÚÕýÈ·µÄ±êÇ©¡£ÏÖÔÚ£¬ÔËÐвâÊÔ¡ª¡ª½á¹ûÊÇËùÓжÏÑÔÓ¦¸Ã¶¼³É¹¦¡£

ÐÔÄܲâÊÔ

¸ù¾ÝÆ»¹û¹«Ë¾¹Ù·½Îĵµ

(https://developer.apple.com/library/prerelease/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/04-writing_tests.html#//apple_ref/doc/uid/TP40014132-CH4-SW8)ÃèÊö£ºÒ»¸öÐÔÄܲâÊÔÐèҪʹÓÃÄãÏëÒªÆÀ¹ÀµÄÒ»¸ö´úÂë¿é£¬²¢ÔËÐд˴úÂë¿é10´Î£¬ÆÚ¼äÊÕ¼¯Æ½¾ùÖ´ÐÐʱ¼äºÍÔËÐеıê׼ƫ²îÖµ¡£ÕâЩ¸ö±ð²âÁ¿µÄƽ¾ùÖµ³ÉΪ²âÊÔÔËÐеÄÒ»¸öÖµ£¬È»ºó°Ñ¸ÃÖµÓëÒ»¸ö»ù×¼Öµ½øÐбȽÏÀ´ÆÀ¹À³É¹¦»òʧ°Ü¡£

дһ¸öÐÔÄܲâÊÔ»¹ÊǷdz£¼òµ¥µÄ©UÄãÖ»ÐèÒª°ÑÄãÏëÒª²âÊԵĴúÂë·Åµ½measure()·½·¨µÄ±Õ°üÖм´¿É¡£

ΪÁËʵ¼ÊÌåÑéһϣ¬ÇëÖØÐ´ò¿ªHalfTunesÏîÄ¿£¬È»ºóÔÚHalfTunesFakeTestsÀàÖÐʹÓÃÏÂÃæµÄ²âÊÔ£¬´Ó¶øÌæ»»µôϵͳĬÈÏÉú³ÉµÄtestPerformanceExample()·½·¨©U

// Performance  
func test_StartDownload_Performance() {
let track = Track(name: "Waterloo", artist: "ABBA",
previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a")
measure {
self.controllerUnderTest?.startDownload(track)
}
}

ÏÖÔÚ£¬ÇëÔËÐÐÉÏÃæµÄ²âÊÔ£¬È»ºóµ¥»÷measure()±Õ°üĩβµÄͼ±êÀ´¹Û¿´Í³¼ÆÐÅÏ¢¡£

µ¥»÷¡°Set Baseline¡±(ÉèÖûù×¼Öµ)°´Å¥£¬È»ºóÔÙ´ÎÔËÐÐÐÔÄܲâÊÔ²¢²é¿´½á¹û¡ª¡ª½á¹ûÓпÉÄܱȻù×¼Öµ¸üºÃ»ò¸üÔã¡£Äã¿ÉÒÔµã»÷Edit(±à¼­)°´Å¥°ïÖúÄú½«»ù×¼ÖµÖØÖÃΪÕâ¸öеĽá¹û¡£

»ù×¼ÖµÔÚÿ¸öÉ豸ÅäÖÃʱ´æ´¢ÆðÀ´£¬ËùÒÔÄã¿ÉÒÔÈÃͬһ²âÊÔÖ´ÐÐÔÚÈô¸Ę́²»Í¬µÄÉ豸ÉÏ£¬²¢Ê¹Ã¿Ì¨É豸±£³ÖÒ»¸ö²»Í¬µÄ»ù×¼Öµ¡ª¡ªÕâҪȡ¾öÓÚ´¦ÀíÆ÷ËÙ¶È¡¢ÄÚ´æµÈµÄ¾ßÌåÅäÖÃÇé¿ö¡£

ÈκÎʱºòÖ»ÒªÄã¸ü¸ÄÒ»¸öÓ¦ÓóÌÐò£¬¶¼ÓпÉÄÜÓ°ÏìÕýÔÚ²âÊԵķ½·¨µÄÐÔÄÜ;Äã¿ÉÒÔÔÙ´ÎÔËÐÐÐÔÄܲâÊÔÀ´¹Û²ìµ±Ç°ÖµÓë»ù×¼Öµ±È½ÏµÄ½á¹û¡£

´úÂ븲¸Ç

´úÂ븲¸Ç¹¤¾ßÄܹ»¸æËßÄãÓ¦ÓóÌÐòÖеÄÄÄЩ´úÂëʵ¼ÊÉϱ»ÄúµÄ²âÊÔÔËÐйý;ÕâÑùÒ»À´£¬Äã¾Í¿ÉÒÔÖªµÀÓ¦ÓóÌÐò´úÂëµÄÄÄЩ²¿·Ö»¹Ã»Óб»²âÊÔ¡£

¡¾×¢Òâ¡¿ÔÚÆôÓôúÂ븲¸Ç¹¦ÄÜʱÄãÊÇ·ñÓ¦¸ÃÔËÐÐÐÔÄܲâÊÔÄØ?Æ»¹û¹«Ë¾µÄÎĵµ(https://developer.apple.com/library/prerelease/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/07-code_coverage.html#//apple_ref/doc/uid/TP40014132-CH15-SW1)ÖÐÊÇÕâÑùÃèÊöµÄ©U´úÂ븲¸ÇÊý¾Ý¼¯ºÏ»áµ¼ÖÂÐÔÄܵÄϽµ¡­¡­ÒÔÏßÐÔ·½Ê½Ó°Ïì´úÂëµÄÖ´ÐÐ;Òò´Ë£¬µ±ÆôÓôúÂ븲¸Ç¹¦ÄÜʱ³ÌÐòµÄÐÔÄܽ«»áÒò²»Í¬µÄ²âÊÔÔËÐжøÓÐËù²îÒì¡£µ«ÊÇ£¬µ±Äã¶ÔÄãµÄ²âÊÔÖеÄÀý³ÌÒªÇ󼫯äÑϸñʱÄãÓ¦¸ÃÈÏÕæ¿¼ÂÇÊÇ·ñÒªÆôÓôúÂ븲¸ÇÖ§³Ö¡£

ΪÁËÆôÓôúÂ븲¸Ç¹¦ÄÜ£¬Äã¿ÉÒԱ༭һÏÂÄãÔ¤Ïȼƻ®µÄ²âÊÔ(Test)²Ù×÷²¢¹´Ñ¡¡°Code Coverage¡±¸´Ñ¡¿ò©U

ÔËÐÐÄúµÄËùÓвâÊÔ(°´ÏÂ×éºÏ¼üCommand+U)£¬È»ºó´ò¿ª±¨¸æµ¼º½Æ÷(°´ÏÂ×éºÏ¼üCommand+8)¡£°´Ö´ÐÐʱ¼äÏȺóÑ¡Ôñ(By Time£¬¼ûÏÂͼ)ÁбíÖÐ×îÉÏÃæµÄÒ»ÏȻºóÔÙÑ¡Ôñ¡°Coverage¡±(¸²¸Ç)Ñ¡Ï¡£

Äã¿ÉÒÔµ¥»÷ÈçÏÂͼչ¿ªµÄÈý½ÇÐÎͼ±êÀ´¹Û²ìSearchViewController.swiftÎļþÖеĺ¯ÊýÁбí©U

Äã¿ÉÒÔ°ÑÊó±êÐüÍ£ÔÚupdateSearchResults(_:)·½·¨¸½½üµÄÀ¶É«µÄCoverage(¸²¸ÇÂÊ)ÌõÉϹ۲쵽¶ÔÓ¦µÄ¸²¸ÇÂÊΪ71.88%¡£

µ¥»÷¸Ãº¯Êý¶ÔÓ¦µÄ¼ýÍ·°´Å¥À´´ò¿ªÔ´Îļþ£¬²¢¶¨Î»µ½¸Ãº¯Êý¡£µ±ÄãµÄÊó±êÒÆµ½ÓÒ±ßÀ¸Öеĸ²¸ÇÂÊ×¢ÊÍÉÏʱ£¬´úÂë¶Î½«Í»³öÏÔʾΪÂÌÉ«»òºìÉ«©U

¸²¸ÇÂÊ×¢ÊÍÉϵÄÐÅÏ¢ÏÔʾ³öÒ»¸ö²âÊÔÖÐÃüÖÐÿ¸ö´úÂë¶ÎµÄ´ÎÊý¡£×¢Ò⣬ûÓб»µ÷Óõ½µÄ´úÂë¶Î²¿·ÖÍ»³öÏÔʾΪºìÉ«¡£ÕýÈçÄãËùÆÚÍûµÄ£¬forÑ­»·ÔËÐÐ3´Î£¬µ«Ã»ÓÐÒ»´ÎÊÇÑØ×Å´íÎó·¾¶Ö´Ðеġ£ÎªÁËÌá¸ß´Ëº¯ÊýµÄ´úÂ븲¸ÇÂÊ£¬Äã¿ÉÒÔ¸´ÖÆabbaData.json£¬È»ºóÐÞ¸ÄËü£¬Ê¹Æä»áµ¼Ö²»Í¬µÄ´íÎ󡪡ªÀýÈ磬½«¡°results¡±¸ü¸ÄΪ¡°result¡±À´²âÊÔÖ´Ðе½´òÓ¡Óï¾äprint("Results key not found in dictionary")µÄÇé¿ö¡£

100%¸²¸Ç?

ÕùȡʵÏÖ100%µÄ´úÂ븲¸ÇÂÊÄã¿ÉÖªµÀÓ¦¸Ã¸¶³öÔõÑùµÄŬÁ¦Âð?Èç¹ûÄãʹÓùȸèËÑË÷ÒýÇæËÑË÷¡°100% unit test coverage¡±µÄ»°£¬Äã»áËÑË÷µ½ÓÐÔÞͬµÄÒ²Óз´¶ÔµÄµÈ¶àÖֹ۵㣬ÒÔ¼°Î§ÈÆ100%¸²¸ÇÂʵĴóÁ¿ÕùÂÛ¡£ÆäÖУ¬³Ö·´¶Ô¿´·¨µÄÈÏΪ×îºóµÄ10-15%²¢²»ÖØÒª¡ª¡ª²»ÖµµÃΪ֮¸¶³öŬÁ¦;¶ø³ÖÔÞͬ¿´·¨µÄÈÏΪ×îºóµÄ10-15%¼«ÆäÖØÒª¡ª¡ªÒòΪËüºÜÄѲâÊÔ¡£ÔÙʹÓùȸèËÑË÷ÒýÇæËÑË÷¡°hard to unit test bad design¡±¿ÉÒÔÕÒµ½ÆÄÓÐ˵·þÁ¦µÄÂ۾ݡª¡ªÎÞ·¨ÑéÖ¤µÄ´úÂëÊÇÒ»ÖÖ¸üÉî²ã´ÎµÄÉè¼ÆÎÊÌâ(https://www.toptal.com/qa/how-to-write-testable-code-and-why-it-matters)¡£½øÒ»²½µÄ˼¿¼¿ÉÄܵ¼ÖµĽáÂÛÊDzâÊÔÇý¶¯¿ª·¢(http://qualitycoding.org/tdd-sample-archives/)ÊÇÈí¼þ¿ª·¢¹ý³ÌÖбØÐëÒª×ߵķ¡£

×ܽá

±¾ÎÄÖÐÒѾ­ÏòÄãÌṩÁËΪÄãµÄiOS¹¤³Ì±àд²âÊԵĶàÖÖ¹¤¾ß¡£ÎÒÏ£ÍûÄãÄܹ»Í¨¹ý±¾½Ì³ÌµÄѧϰÊ÷Á¢Æð×ã¹»µÄÐÅÐÄÀ´²âÊÔÒ»ÇÐ!

 

   
3408 ´Îä¯ÀÀ       28
Ïà¹ØÎÄÕÂ

΢·þÎñ²âÊÔÖ®µ¥Ôª²âÊÔ
һƪͼÎÄ´øÄãÁ˽â°×ºÐ²âÊÔÓÃÀýÉè¼Æ·½·¨
È«ÃæµÄÖÊÁ¿±£ÕÏÌåϵ֮»Ø¹é²âÊÔ²ßÂÔ
È˹¤ÖÇÄÜ×Ô¶¯»¯²âÊÔ̽Ë÷
Ïà¹ØÎĵµ

×Ô¶¯»¯½Ó¿Ú²âÊÔʵ¼ù֮·
jenkins³ÖÐø¼¯³É²âÊÔ
ÐÔÄܲâÊÔÕï¶Ï·ÖÎöÓëÓÅ»¯
ÐÔÄܲâÊÔʵÀý
Ïà¹Ø¿Î³Ì

³ÖÐø¼¯³É²âÊÔ×î¼Ñʵ¼ù
×Ô¶¯»¯²âÊÔÌåϵ½¨ÉèÓë×î¼Ñʵ¼ù
²âÊԼܹ¹µÄ¹¹½¨ÓëÓ¦ÓÃʵ¼ù
DevOpsʱ´úµÄ²âÊÔ¼¼ÊõÓë×î¼Ñʵ¼ù
×îл¼Æ»®
DeepSeekÔÚÈí¼þ²âÊÔÓ¦ÓÃʵ¼ù 4-12[ÔÚÏß]
DeepSeek´óÄ£ÐÍÓ¦Óÿª·¢Êµ¼ù 4-19[ÔÚÏß]
UAF¼Ü¹¹ÌåϵÓëʵ¼ù 4-11[±±¾©]
AIÖÇÄÜ»¯Èí¼þ²âÊÔ·½·¨Óëʵ¼ù 5-23[ÉϺ£]
»ùÓÚ UML ºÍEA½øÐзÖÎöÉè¼Æ 4-26[±±¾©]
ÒµÎñ¼Ü¹¹Éè¼ÆÓ뽨ģ 4-18[±±¾©]

LoadRunnerÐÔÄܲâÊÔ»ù´¡
Èí¼þ²âÊÔ½á¹û·ÖÎöºÍÖÊÁ¿±¨¸æ
ÃæÏò¶ÔÏóÈí¼þ²âÊÔ¼¼ÊõÑо¿
Éè¼Æ²âÊÔÓÃÀýµÄËÄÌõÔ­Ôò
¹¦ÄܲâÊÔÖйÊÕÏÄ£Ð͵Ľ¨Á¢
ÐÔÄܲâÊÔ×ÛÊö


ÐÔÄܲâÊÔ·½·¨Óë¼¼Êõ
²âÊÔ¹ý³ÌÓëÍŶӹÜÀí
LoadRunner½øÐÐÐÔÄܲâÊÔ
WEBÓ¦ÓõÄÈí¼þ²âÊÔ
ÊÖ»úÈí¼þ²âÊÔ
°×ºÐ²âÊÔ·½·¨Óë¼¼Êõ


ij²©²ÊÐÐÒµ Êý¾Ý¿â×Ô¶¯»¯²âÊÔ
IT·þÎñÉÌ Web°²È«²âÊÔ
IT·þÎñÉÌ ×Ô¶¯»¯²âÊÔ¿ò¼Ü
º£º½¹É·Ý µ¥Ôª²âÊÔ¡¢Öع¹
²âÊÔÐèÇó·ÖÎöÓë²âÊÔÓÃÀý·ÖÎö
»¥ÁªÍøweb²âÊÔ·½·¨Óëʵ¼ù
»ùÓÚSeleniumµÄWeb×Ô¶¯»¯²âÊÔ