標籤:

簡單重構,顯著效果----提升scala自動化測試效率

如何提升Scala 測試效率

一 為何提高?

敏捷對於單元測試和集成測試的自動化運行效率要求很高,期望在短時間裡完成代碼提交後的用例運行。常常會存在這種情況,代碼運行時間很短,但是自動化用例耗時很長,一個用例執行可能不到一秒鐘,但是在自動化中可能要超過好幾秒,整個自動化效率低下,這是自動化測試中常見的場景。

本文結合項目實際,談談如何提高測試效率。

先看下我們的數據:

190個用例,CI在性能好的機器上運行花費時間8分半(512s),而在同台機器上scala統計只有26s,差距巨大。在性能不好的機器上運行CI測試需要20分鐘,這效率無法滿足敏捷開發。

為了提升效率,通過大量的分析和代碼重構,使得最終的效率提升將近11倍,以前8分半左右的CI用例執行時間,重構後不到1分鐘即可運行完畢。

如何提高?

軟體上,提升測試效率可以從以下幾方面進行考慮

  • 測試系統啟動效率
  • 用例組合運行效率
  • 單步驟用例執行效率
  • 並行運行用例

下面重點講述如何重構測試用例代碼來提升效率

為了減少文章的篇幅,下文代碼以一個簡單的測試類來說明,相關的運行時長也是簡單類的運行時長,在實際項目中,各步驟的運行時長比例子更長。如創表操作平均在2.5s

  • 測試系統啟動效率

測試系統或被測對象有時需要啟動一些所需的伺服器,此時,可以採取mock的方式,或者整體啟動一次(見下)。

  • 用例組合運行效率

整體用例運行效率和用例的編寫有很大關係,例如,目前的用例大多如下編寫,

單個測試class實現方式:

  1. class UsergroupActorTest extends FunSpec with Matchers with BeforeAndAfterEach
  2. with StoragePrepare with TablesDBServicePrepare{
  3. override protected def beforeEach(): Unit = {
  4. super.beforeEach()
  5. UsergroupActor.actorOf()
  6. }
  7. describe("UsergroupActorTest") {
  8. it("handleSubSystemInfoGetRecord"){
  9. val request: String = TestData.subSystemInfopostrecord1
  10. var result = UsergroupActor.handleSubSystemInfoPostRecord(request)
  11. result.status should be(ResponseStatus(200, "OK"))
  12. result = UsergroupActor.handleSubSystemInfoGetRecord()
  13. }
  14. it("handleGetSubSystemInfoByGroupname"){
  15. val subsysRecord = Array[String]("IOT","app","IoT","IOT APP","http://10.63.246.83:2016","true")
  16. SubSystemInfoDBService().insertRecord(subsysRecord) should be(true)
  17. val userGroupRecord =
  18. Array[String]("0","individual","[{"subsystem":"IOT","rolename":["individual"]}]")
  19. UserGroupRolesDBService().insertRecord(userGroupRecord) should be(true)
  20. val result = UsergroupActor.handleGetSubSystemInfoByGroupname("individual")
  21. result.status should be(ResponseStatus(200, "OK"))
  22. }
  23. }

其中:

StoragePrepare :每個it運行前連接資料庫,運行後如果表存在則整個刪除,同時記錄也被刪除

  1. trait StoragePrepare extends ScalatraSuite with BeforeAndAfterEach {
  2. private val dbName = "uTest"
  3. override protected def beforeEach(): Unit = {
  4. super.beforeEach()
  5. DBService.connect("localhost", "root", "", "3306", dbName)
  6. }
  7. override protected def afterEach(): Unit = {
  8. super.afterEach()
  9. DBService.execute(SQL(s"drop database if exists $dbName"))
  10. }
  11. }

TablesDBServicePrepare:數據準備,包括創建數據表,以及插入基礎表數據

  1. trait TablesDBServicePrepare extends ScalatraSuite with BeforeAndAfterEach {
  2. override protected def beforeEach(): Unit = {
  3. super.beforeEach()
  4. UserGroupsDBService().createTable()
  5. UserGroupsDBService().insertRecord(Array[String]("0","individual"))
  6. UsersDBService().createTable()
  7. UsersDBService().insertRecord(Array[String]("0","admin", "admin123", "test@test.com",
  8. "18254565825","cop","shanghai","individual","1"))
  9. UsersDBService().insertRecord(Array[String]("0","admin2", "admin123", "test@test.com",
  10. "18254565825","cop","shanghai","individual","1"))
  11. SubSystemInfoDBService().createTable()
  12. SubSystemRolesDBService().createTable()
  13. UserGroupRolesDBService().createTable()
  14. }
  15. }

整體運行過程如下,(以創5個簡單的表為例,數字為運行平均時長,單位ms)

問題在哪?

1 一個測試集下,測試前後的動作有通用部分,需要在suite間、suite內、it內進行提煉。

以上的測試方式,將創建表和插數據混在一起作為每個it的操作,導致it操作每次都進行啟動服務,連接資料庫、創表動作,降低了用例執行效率,同時在代碼結構上也不清晰。創建表應該是suite一次性動作,用beforeAll來實現。

2 it測試單獨測試和多個it測試串列測試時,滿足童子軍軍規,以減少相互影響。

上述運行採用里簡單粗暴的方式,it只能在表的層次上刪除整個表,無法採用清除記錄,其原因正是beforeEach中的表和記錄同時操作。

3 在領域層次方面的劃分比較混亂,如果多個it測試要求的數據完全相同,可以進行子suite的拆分。

改造後,連接伺服器、創表、表記錄初始化、刪除表都作為獨立trait,在suite和it層次上進行合理調用。執行流程如下

重構後代碼

  1. class UsergroupActorTest extends FunSpec with Matchers with BeforeAndAfterEach
  2. with StoragePrepareOnceJetty with TablesDBServiceCreateTableOnce with TablesDBServiceRecordPrepare{
  3. //it先置條件
  4. override protected def beforeEach(): Unit = {
  5. super.beforeEach()
  6. UsergroupActor.actorOf()
  7. }
  8. override protected def afterEach(): Unit = {
  9. super.afterEach()
  10. //clear the records which created in this test
  11. UsersDBService().updateEx("delete from users")
  12. UserGroupsDBService().updateEx("delete from usergroups")
  13. SubSystemInfoDBService().updateEx("delete from subsysteminfo")
  14. UserGroupRolesDBService().updateEx("delete from usergrouproles")
  15. }
  16. describe("UsergroupActorTest") {
  17. it("handleSubSystemInfoGetRecord"){
  18. ...... //test code here
  19. }
  20. it("handleGetSubSystemInfoByGroupname"){
  21. ...... //test code here
  22. }
  23. }
  24. }
  25. trait StoragePrepareOnce extends ScalatraSuite with BeforeAndAfterAll {
  26. private val dbName = "uTest"
  27. override protected def beforeAll(): Unit = {
  28. super.beforeAll()
  29. DBService.connect("localhost", "root", "", "3306", dbName)
  30. }
  31. override protected def afterAll(): Unit = {
  32. super.afterAll()
  33. DBService.execute(SQL(s"drop database if exists $dbName"))
  34. }
  35. }
  1. trait TablesDBServiceCreateTableOnce extends ScalatraSuite with BeforeAndAfterAll {
  2. override protected def beforeAll(): Unit = {
  3. super.beforeAll()
  4. UserGroupsDBService().createTable()
  5. UsersDBService().createTable()
  6. SubSystemInfoDBService().createTable()
  7. SubSystemRolesDBService().createTable()
  8. UserGroupRolesDBService().createTable()
  9. }
  10. }
  1. trait TablesDBServiceRecordPrepare extends ScalatraSuite with BeforeAndAfterEach {
  2. override protected def beforeEach(): Unit = {
  3. super.beforeEach()
  4. UserGroupsDBService().insertRecord(Array[String]("0","individual"))
  5. UsersDBService().insertRecord(Array[String]("0","admin", "admin123", "test@test.com",
  6. "18254565825","cop","shanghai","individual","1"))
  7. UsersDBService().insertRecord(Array[String]("0","admin2", "admin123", "test@test.com",
  8. "18254565825","cop","shanghai","individual","1"))
  9. }
  10. }

這樣優化後,大量的減少了重複操作,單測試class的重構基本完成。

那麼,對於一個project,多個測試類怎麼處理呢?我們需要繼續優化下去。

同樣的思想,在不同文件(class suites)共用前置操作和後置操作。

通過Suites(new UsergroupActorTest, new UsersCfgActorTest)將幾個測試類整合到一個測試類中,有兩種實現,

1 類

  1. class NestSuiteTest extends Suites(new UsergroupActorTest, new UsersCfgActorTest) with BeforeAndAfterAll{
  2. override protected def beforeAll(): Unit = {
  3. super.beforeAll()
  4. println("-------NestSuiteTest act firstly within whole project")
  5. }
  6. override def afterAll() {
  7. super.afterAll()
  8. println("write ur action here!") //所有suites運行完後,一次性回收動作
  9. }
  10. }
  11. @DoNotDiscover
  12. class UsergroupActorTest extends FunSpec with Matchers with BeforeAndAfterEach
  13. with StoragePrepareOnceJetty with TablesDBServiceCreateTableOnce with TablesDBServiceRecordPrepare{
  14. ……
  15. }

我們來看看gradle運行結果:

NestSuiteTest.NestSuiteTest NestSuiteTest.NestSuiteTest$$anon$1.UsergroupActorTest handleUserGroupPostRecord STANDARD_OUT

NestSuiteTest.NestSuiteTest NestSuiteTest.NestSuiteTest$$anon$2.UsersCfgActorTest handleuserpostrecord STANDARD_OUT

ActorTest.usersManage.UsersCfgActorTest UsersCfgActorTest handleuserpostrecord STANDARD_OUT

ActorTest.usersManage.UsergroupActorTest UsergroupActorTest handleUserGroupPostRecord STANDARD_OUT

發現了嗎,UsergroupActorTest,UsersCfgActorTest都運行了兩遍。這下暈菜了,這個咋辦?

好吧,換種寫法

抽象類

  1. class NestSuiteTest extends Suites(new UsergroupActorTest{}, new UsersCfgActorTest{}) with BeforeAndAfterAll{
  2. override protected def beforeAll(): Unit = {
  3. super.beforeAll()
  4. println("-------NestSuiteTest act firstly within whole project")
  5. }
  6. override def afterAll() {
  7. super.afterAll()
  8. println("write ur action here!") //所有suites運行完後,一次性回收動作
  9. }
  10. }
  11. @DoNotDiscover
  12. abstract class UsergroupActorTest extends FunSpec with Matchers with BeforeAndAfterEach
  13. with StoragePrepareOnceJetty with TablesDBServiceCreateTableOnce with TablesDBServiceRecordPrepare{
  14. ……//抽象類
  15. }

我們再來看看gradle運行結果:

NestSuiteTest.NestSuiteTest NestSuiteTest.NestSuiteTest$$anon$1.UsergroupActorTest handleUserGroupPostRecord STANDARD_OUT

NestSuiteTest.NestSuiteTest NestSuiteTest.NestSuiteTest$$anon$2.UsersCfgActorTest handleuserpostrecord STANDARD_OUT

執行結果顯示,只執行了一遍,但是,IDEA中單獨執行UsergroupActorTest也成了泡影,因為abstract class UsergroupActorTest,你懂的。

這也是條死路啊,由此牽扯出另外一個問題,如何執行指定測試類

如何執行指定測試類

如果你知道,可以果斷跳過

先看看gradle的幫助,gradle help —task :test 查看任務test的幫助,

幫助會告訴 —tests Sets test class or method name to be included, 『*』 is supported.

網上一邊倒的說用這個參數可以運行單個測試類

但是,但是,這個是個大餡餅啊

在非SBT方式安裝的scala下,這個寫法完全無效,依然是所有的suite玩命地運行,而碼農們只能苦苦等待。不靠譜的幫助。沒招,只好轉投IDEA,

在IntelliJ IDEA中運行用例的方式(gradle沒配置junit),

  • 如果用例編寫採用@RunWith(classOf[JUnitRunner]),則可以選擇在gradle、Junit或scalatest下運行,並在IntelliJ IDEAJunit下自動添加配置,後續默認以第一次選擇的方式執行,直到手工刪除該配置。如果選了gradle方式,杯具了,後面還是得所有用例全部執行。
  • 如果用例編寫未指定JUnitRunner,IntelliJ IDEA默認調用scalatest運行。此情況下,命令行gradle test則無法運行該用例
  • 如果在gradle配置下加入運行,實際效果就是全用例運行。

好吧,為了自動化,只能採用在IDEA運行單個suite,在集成CI中運行所有用例。

BUT,不死心啊,因為gradle下無法指定class運行,意味著NestSuite必須本身和子集都運行一遍,等於重複運行了一遍。搜啊搜,試啊試,蒼天不負有心人啊,最後,終於找到了 gradle下單獨運行測試類的方法,(關鍵先生來了)

gradle -Dtest.single=NestSuiteTest(指定測試類) test -i

雖然是個就命令,但是管用,真的管用,看著下面的運行結果,這才長舒口氣,忍不住要問候幾句,

NestSuiteTest.NestSuiteTest NestSuiteTest.NestSuiteTest$$anon$1.UsergroupActorTest handleUserGroupPostRecord STANDARD_OUT

NestSuiteTest.NestSuiteTest NestSuiteTest.NestSuiteTest$$anon$2.UsersCfgActorTest handleuserpostrecord STANDARD_OUT

至此,我們測試用例重構大功告成。

  • 單用例執行效率

這個和實際測試內容有關,八仙過海各顯神通,改進不具備借鑒性,略過。

  • 並行運行測試用例

並行運行可通過CI將用例在不同機器分布運行,或者採用用例並行運行的方式,略

推薦閱讀:

python selenium2自動化測試系列電子書
在Selenium Webdriver中使用XPath Contains、Sibling函數定位

TAG:自動化測試 |