Testing #2: Testleri etiketleme ve fixture kullanimi


Scala-test bizlere testleri taglama ve daha sonra bu taglara gore filtreleyerek calistirma imkani sunuyor. Ornek olarak bazi testler yavas seklinde etiketlenebilir ve gerektigi zamanlarda bu testler calistirilmayabilir.

Testi gozardi etme
Default olarak tanimlanmis bir tag ise ignore. Bunu direk kullanmaya baslayabilirsiniz ve o test gozardi edilecektir. 

ignore should "soyledir boyledir" in {
    // baya bi testler ama ignore edilecek
}

sbt test ile calistirdigimizda bu testin yesil yerine sari olarak gectigini ve yaninda IGNORED yazdigini gorecegiz. 

Kendi tagimizi uretiyoruz
Tag tanimlamak icin Tag traitini extend eden bir object yaratmamiz yeterli. Daha sonra bunu teste eklerken her style trait (yani kullandigimiz test stili) kendi yontemini sunar.

import org.scalatest.Tag
object DatabaseTesti extends Tag("com.mycompany.tags.DbTest")

Daha sonra ornegin FlatSpec'te bu tagi bir teste eklemek istersek:

"Acayip database" should "degisik olaylara gebe" taggedAs(DatabaseTesti, Slow) in {
    // teste gel
}

seklinde yapabiliriz. Bu arada Slow taginin halihazirda tanimlanmis bir tag oldugunu goruyoruz. 

Testleri calistirma
scala-test testilerini calistirmanin bircok yolu var. Biz burada sbt ile ileryecegiz. Simdi birkac komuta gozatalim. Bunlari sbt shell'deyken ya da komut satirindan sbt'ye parametre vererek calistirabiliriz.

test : tum testleri calsitir
testOnly: com.bizimsirket.proje.SuperTestimiz : sadece verilen test suit'ini calistir
test-quick : sadece son yapilan degisiklikler sonucu etkilenen testleri calistir.

Default olarak sbt testleri paralel calistirir. sbt'nin kendisine ait bir thread pool'u vardir. Amprpik olarak bunun boyutu sistemdeki cpu sayisi * 2 seklinde hesaplanir. 4 core'lu bir bilgisayarda calisan testler icin thread pool size'i 8 olacaktir. Eger bu istenmiyor ise, testlerin seri olarak calistirilmasi icin

parallelExecution in Test := false

ayarlamasi yapmak gerekir. 

Testleri filtreleme
testOnly komutuna classpath filtresi ve tag listesi belirtip, testleri filtreleyerek calistirabiliriz. -n ile calistirilacak, -l ile de calistirilmayacak testleri belirtebiliyoruz. 

testOnly org.acme.*-- -n FunctionalTests -l org.scalatest.tags.Slow

Eger dahil edilecek testler belirtilmemis ise classpath filtresine uyan tum testler dahil edilir ve -l ile belirtilenler exclude edilir. 

Fixture
Fixture, testlerde kullanilan scala object ve diger artifactlardir (dosya, socket, database baglantisi ... ). Eger birden fazla test bir artifacti kullaniyor ise, bunlari tek bir yerde toplayip duplikasyonu onlemek onemlidir. Ne kadar fazla test hardcoded olarak bu artifactlari iceriyor ise refaktor etmek zorlasacaktir.

Testlerdeki bu kod duplikasyonunu onlemek icin onerilen 3 yontem vardir:
1- scala test kodunu refaktor etmek
2- withFixture traiti kullanmak
3- before ve after traitleri kullanmak

Bu arada kod duplikasyonunu onlerken, testler arasinda shared mutable state olusturmak cok daha tehlikelidir. Buna da dikkat etmek gerekir. 

1. Kod refaktor
Birinci yontem olarak birden fazla test tarafindan kullanilan fixture'lari bir metot seklinde extract edebilir ve testler icerisinden cagirabiliriz. 

class ExampleSpec extends AnyFlatSpec {
  def fixture =
    new {
      val builder = new StringBuilder("ScalaTest is ")
      val buffer = new ListBuffer[String]
    }

  "Testing" should "be easy" in {
    val f = fixture
    f.builder.append("easy!")
    assert(f.builder.toString === "ScalaTest is easy!")
    assert(f.buffer.isEmpty)
    f.buffer += "sweet"
  }
}

Bu sekilde istedigimiz test icerisinden fixture metodunu cagirdigimizda builder ve bufferin yeni birer instancelarini olusturmus olacagiz. Bu saydede testler arasinda mutable state paylasilmamis olacak. Ama yaratilan bu artifactlari test bittikten sonra temizlememiz gerekiyorsa bu konuda bize yardimci olmaz, onu elle yapmamiz gerekir. 

Diger bir yontem de gerekli fixture'lari traitler seklinde konumlandirmak ve gerekli testlerde bu traitleri olaya dahil etmek:

class ExampleSpec extends AnyFlatSpec {
  trait Builder {
    val builder = new StringBuilder("ScalaTest is ")
  }
  trait Buffer {
    val buffer = ListBuffer("ScalaTest", "is")
  }
  "Testing" should "be productive" in new Builder {
    builder.append("productive!")
    assert(builder.toString === "ScalaTest is productive!")
  }
    // birden fazla fixture trait de kombine edilebilir
    it should "be clear and concise" in new Builder with Buffer {
           //...
    }

}

Bir onceki metota tanimladigimiz artifactlar tum testler tarafindan paylasilabilecekken bu yontemde sadece gereken testlere bu artifactlari kullanabilme imkani tanimis oluyoruz. Ama yine onceki metot gibi fixture artifactlarini kullandiktan sonra temizlemek gerekiyorsa bize cok yardimi olmayacak. 

Eger bir teste fixture artifacti pass edip test bittikten sonra da temizleme yapmamiz gerekiyorsa bu durumda loan pattern'inin kullanabiliriz. Bunun icin load-fixture metotlari yazmamaiz gerekiyor. Bu metot fixture artifact'ini olusturuyor, test tarafindan kendisine odunc verilen (loan) kod parcasini calistiriyor ve daha sonra da cleanup adimlarini tamamliyor. Su sekilde:

class ExampleSpec extends AnyFlatSpec {
  def withFile(testCode: (File, FileWriter) => Any) {
    val file = File.createTempFile("hello", "world") // fixture'i olustur
    val writer = new FileWriter(file)
    try {
      writer.write("ScalaTest is ") 
      testCode(file, writer) // fixture'i teste odunc ver (loan)
    }
    finally writer.close() // fixture'i temizle
  }
  "Testing" should "be productive" in withFile { (file, writer) =>
    writer.write("productive!")
    writer.flush()
    assert(file.length === 24)
  }
// loan-fixture metodlari compose edilebilir, birlestirilebilir
  it should "be clear and concise" in withDatabase { db => 
    withFile { (file, writer) => 
      db.append("clear!")
      writer.write("concise!")
      writer.flush()
      assert(db.toString === "ScalaTest is clear!")
      assert(file.length === 21)
    }
  }
}

Burada withFile isimli loan-fixture metodunu olusturduk. Bu metot kendisine pass edilen bir kod parcasini (ornekte testCode isimli arguman) calistiriyor. Ama bunu yapmadan once gerekli artifactlari (burada test dosyasi) olusturuyor ve kod parcasi bittikten sonra da cleanup (dosya writer'inin kapatilmasi) yapiyor. Testin icerisinde bu metodu cagiriyoruz ve kendisine gerceklestirmek istedigimiz testleri testCode argumani seklinde pass ediyoruz. 

 Son ornektede de loan-fixture metotlarinin compose edilbilip birlikte kullanilabilecegini gostermis olduk. Bu sayede test icerisinde fixture artifact olusturma ve test bittikten sonra cleanup yapma kisimlarini barindirmamis olduk. 

2. scala-test withFixture
 Yine benzer sekilde eger tum testlerde ayni fixture kullanilacaksa salcatest'in Suite class'inda bulunan withFixture metodunu overwrite ederek de ayni etkiyi elde edebiliriz.

class ExampleSpec extends AnyFlatSpec {
  override def withFixture(test: NoArgTest) = {
    // 1. gerekli fixture'lari olustur, side-effectleri hallet
    // 2. testi calistir ve sonuca gore gerekli clean-up'i yap
    super.withFixture(test) match {
      case failed: Failed =>
        val currDir = new File(".")
        val fileNames = currDir.list()
        info("Dir snapshot: " + fileNames.mkString(", "))
        failed
      case other => other
    }
  }
  it should "fail" in {
    assert(1 + 1 === 3)
  }
}

Bu sayede test tanimlamasinda ekstra bir trait ya da metot kullanmadan, tum testlerin calismadan once gerekli fixture olusturma asamalarini tanimlayabiliriz. Hatta burada test fail olduktan sonra mevcut klasorun dosya listesini de ekrana bastiriyoruz, bu sekilde test icin olusturulan side-effectler hakkinda debug bilgisi de verebiliriz. 

3. BeforeAfter traiti
Su  ana kadar inceledigimiz yontemlerde fixture artifactlarinin olusturulmasi, setup edilmesi ve de cleanup edilmesi hep test esnasinda gerceklesti. Yani egere bu asmalarda bir problem cikarsa test fail olur. Ama bazen fixture olusturme isleminin test baslamadan once ve cleanup isleminin de test bittikten sonra yapilmasini ve eger bu asamalarda bir problem olursa daha fazla testin calistirilmamasini isteyebilirz. Bu durumda testler icin before ve after seklinde calistirilmasi gereken kod parcalarini tanimlayabiliriz. 

class ExampleSpec extends AnyFlatSpec with BeforeAndAfter {
  val builder = new StringBuilder
  val buffer = new ListBuffer[String]
 
before {
    builder.append("ScalaTest is ")
  }

  after {
    builder.clear()
    buffer.clear()
  }

  "Testing" should "be easy" in {
    builder.append("easy!")
    assert(builder.toString === "ScalaTest is easy!")
    assert(buffer.isEmpty)
    buffer += "sweet"
  } 
}

Seklinde her bir testten once gerekli adimlari before ve test bittikten sonra gerceklestirilmesi gereken adimlari da after metodlarinda belirttik. Ama goruyoruz ki before, after ve test metodlarinin haberlesmesi mutable state uzerinden gerceklesiyor. Ya var ile mutable nesneler tanimlanmasi gerek ya da val ile tanimlanmis nesnelerin (yukaridaki ornekteki gini) state'inin mutate edilmesi gerekiyor. Ama bu durumda da testleri paralel olarak calistiramiyor olmamiz gerekir. Cunku ayni shared statei kullanan kod bloklarini paralel calistirirsak nasil sonuclar ile karsilacagimizi tahmin etmek zor olacaktir. Scala-test burada her bir test icin , test classinin yeni bir instance'ini olusturuyor. Bu sayede her bir test kendi test classi instance icerisinde (bir nevi sandbox) calismis oluyor ve testler birbirlerinin stateinin mutate etmemis oluyor. (ParallelTestExecution tratinin, OneInstancePerTest traitinden turemesinde bu yontemi gorebiliriz.)

Stacking Traits
Buyuk projelerde test class'larina trait ile fixture saglamak, bunu da yaparken init ve cleanup'lari da handle etmek daha avantajli olabilir. 

SuiteMixin traitini extend eden traitler icersinde fixture'larimizi olusturabilir ve gereken test class'larini bu traitimiz ile extend edebiliriz. Bu durumda bu test class'i icerisindeki tum testler fixture'a ulasabilir. 

trait Buffer extends SuiteMixin { this: Suite =>
  val buffer = new ListBuffer[String]

  abstract override def withFixture(test: NoArgTest) = {
    try super.withFixture(test)
    finally buffer.clear()
  }
}

class ExampleSpec extends AnyFlatSpec with Buffer {
  "Testing" should "be easy" in {
    builder.append("easy!")
    assert(builder.toString === "ScalaTest is easy!")
    assert(buffer.isEmpty)
    buffer += "sweet"
  }
}

SuiteMixin'i extend etmek bize withFixture metodunu override etme imkani getiriyor. Bu sayede fixture'lari once initialize edip (ornekte bir ListBuffer olusturuluyor) daha sonra testi kosup son olarak da clean up yapabiliyoruz (ornekte buffer'in temizlenmesi). Buffer traiti gibi baska traitler de olusturabilir ve test classini bunlar ile extend edebiliriz. Bu yaklasima stacking traits yani bir nevi yigin gibi yigilan traitler adi veriliyor. Ayrica extend sirasi, bu traitlerin calistirilma sirasini belirler. 

Diger, belki de daha da okunabilir bir stacking traits yaklasimi da BeforeAndAfterEach traitini extend etmekdir. Ornekle gorelim:

trait Buffer extends BeforeAndAfterEach { this: Suite =>

  val buffer = new ListBuffer[String]

  override def afterEach() {
    try super.afterEach() // To be stackable, must call super.afterEach
    finally buffer.clear()
  }
}

class ExampleSpec extends AnyFlatSpec with Buffer {
  "Testing" should "be easy" in {
    builder.append("easy!")
    assert(builder.toString === "ScalaTest is easy!")
    assert(buffer.isEmpty)
    buffer += "sweet"
  }
}

 Her bir testten once ve sonra calistiirlmasi gereken adimlari afterEach ve beforeEach metotlarini override ederek Buffer traitinde tanimladik. Simdi biz bu trait ile hangi test classini extend edersek, o test classi icerisindeki testler calistirilmadan once ve sonra neler yapilmasi gerektigini tanimlamis oluruz. Gayet plugggable bir yaklasim, super.

Ozet:
- testleri ignore etme, tag ile etiketleme ve bu etiketlere gore calistirma 
- fixture nedir
- uc farkli yontem ile scala kodunu refactor ederek fixture'lari testler arasinda share etme
- scala-test'in withFixture metodunu kullanma
- BeforeAfter traitini kullanma
- Stacking traits yaklasimi


Yorumlar

Bu blogdaki popüler yayınlar

Python'da Multithreading ve Multiprocessing

Threat Modeling 1

Encoding / Decoding