Testing #1: Test stili ve assert


Bu dizide genel olarak scala ve spark projelerini nasil test ederiz buna deginecegiz. 

Scala Test
Gunumuzde scala projelerinde defacto testing kutuphanesi olarak scala-test kutuphensini kullaniliyor. Birkac noktaya burada deginelim:

- Scala-test birden fazla testing stiline izin vermektedir. Ancak bir proje icerisinde bunlari karisik olarak kullanmak onerilmez. Proje baslangicinda secilen bir stil tum unit testlerde uygulanmasi daha verimli olur. 

- Farkli test kategorileri icin (ornek olarak unit testing ve acceptance testing) farkli test stilleri kullanilabilir. Hatta bu sayede developer teste baktigi zaman bunun hangi tip test oldugunu daha iyi anlayabilir. 

- Assertion, matcher, mixin ve trait gibi mekanikler tum test stillerinde ayni sekilde calismaktadir. Yani testin icini degistirmeye gerek yoktur. 

- Scala-test'in dokumantasyonda onerdigi yontem unit ve intergration testing icin FlatSpec ve de acceptance test icin ise FeatureSpec seklinde. Bu testing stillerine bir goz atalim. Tam aciklamalari buradan okuyabilirsiniz.

FunSuite Stili
xUnit tarzi testingle alisik olan takimlar icin konforlu bir secim olacaktir. Aciklayici test basliklari yazilmasina olanak tanir ve ciktisi proje gereksinim doumanini andirdigi icin stake-holder'lar ile iletisimde kolaylik saglar. (BDD stili development)

import org.scalatest.funsuite.AnyFunSuite

class SetSuite extends AnyFunSuite {
  test("Bos setin size'i sifir olmalidir") {
    assert(Set.empty.size == 0)
  }
}

Sadece scalatest-funsuite artifactini import ederek FunSuite'i kullanabilirsiniz. Egere scalatest olarak import edersek tum stilleri projede kullanabilmekteyiz. 

FlatSpec Stili
Flat yapisi (yani ic ice olmayan testler) ile basit ve temiz bir stil sunar. Testler x should y seklinde yazilmalidir. Arada mecburi bir should oldugu icin buradaki test aciklamalarini Turkce yazinca sacma oluyor. 

import org.scalatest.flatspec.AnyFlatSpec

class SetSpec extends AnyFlatSpec {
  "An empty Set" should "have size 0" in {
    assert(Set.empty.size == 0)
  }
}

FunSpec Stili
Rubi'nin RSpec kutuphanesine benzer bir test stili sunmaktadir. Nested testler yazilabilmesi, describe, it gibi keywordler ile testlere yapi kazandirabilmesi acisindan BDD'yi tam olarak uygulamak isteyen takimlar icin uygundur. Ciktisi tam da bir gereksinim dokumani (spec) gibi olacaktir. 

import org.scalatest.funspec.AnyFunSpec

class SetSpec extends AnyFunSpec {
  describe("A Set") {
    describe("when empty") {
      it("should have size 0") {
        assert(Set.empty.size == 0)
      }
    }
  }
}

WordSpec Stili
Bana sanki FlatSpec ile FunSpec'in kirmasi gibi gorunen stil. Nested yapiya izin veriyor ve de test aciklamalari cok serbest.

import org.scalatest.wordspec.AnyWordSpec

class SetSpec extends AnyWordSpec {
  "A Set" when {
    "empty" should {
      "have size 0" in {
        assert(Set.empty.size == 0)
      }
   }
  }
}

FreeSpec Stili
WordSpec'in daha da artik serbest olan versiyonu. Burada artik should, when gibi aciklayici keywordler de kullanilmiyor ve tum aciklamalar developer'a birakiliyor. Dolayisi ile BDD konusunda tecrubeli takimlarin harci.

import org.scalatest.freespec.AnyFreeSpec

class SetSpec extends AnyFreeSpec {
  "A Set" - {
    "when empty" - {
      "should have size 0" in {
        assert(Set.empty.size == 0)
      }
    }
  }
}

PropSpec Stili
Unit ve integration testing icin ornegin FlatSpec secilmis ise PropSpec de test matrix yazimini icin kullanilabilir. 

import org.scalatest._
import matchers._
import prop._
import scala.collection.immutable._

class SetSpec extends AnyPropSpec with TableDrivenPropertyChecks with should.Matchers {

    val examples =
        Table(
        "set",
        BitSet.empty,
        HashSet.empty[Int],
        TreeSet.empty[Int]
        )

    property("an empty Set should have size 0") {
        forAll(examples) { set =>
        set.size should be (0)
        }
    }
}

FeatureSpec Stili
Kodda da goruelecegi gibi kullanici hikayesi seklinde test yazimina olanak verir. Bu acidan acceptance testing icin gelistirilmistir. 

import org.scalatest._

class TVSet {
  private var on: Boolean = false
  def isOn: Boolean = on
  def pressPowerButton() {
    on = !on
  }
}

class TVSetSpec extends AnyFeatureSpec with GivenWhenThen {

  info("As a TV set owner")
  info("I want to be able to turn the TV on and off")
  info("So I can watch TV when I want")
  info("And save energy when I'm not watching TV")

  feature("TV power button") {
    scenario("User presses power button when TV is off") {

      Given("a TV set that is switched off")
      val tv = new TVSet
      assert(!tv.isOn)

      When("the power button is pressed")
      tv.pressPowerButton()

      Then("the TV should switch on")
      assert(tv.isOn)
    }
}

RefSpec Stili
Bir stilden cok optimizasyondur. Diger testler gibi function literal icermedigi ve testleri direkt olarak fonksiyon sekilde tanimladigi icin daha hizli compile edilir. Buyuk projelerde compile time azaltilmasinda fayda saglayabilir. Ozelikle static code generator barindiran projelerde test sayisi cok artabilir, bu durumda RefSpec toplamda bir performans artisi getirebilir. 

import org.scalatest.refspec.RefSpec

class SetSpec extends RefSpec {

  object `A Set` {
    object `when empty` {
      def `should have size 0` {
        assert(Set.empty.size == 0)
      }
    }
  }
}

Assertionlar
Scala-test uc cesit assertion'u test stilinden bagimsiz olarak sunmaktadir: assert, assertResult ve assertThrows. Bunlara kisaca deginelim: 

1. assert
Bir scala programinda assert metodunu bir boolean ifade ile cagirabilirsiniz. Eger true donerse asser normal bir sekilde devam edecektir. Ama false donerse scala'nin assert metodu AssertionError hatasi firlatir. Scala assert metodu Predef objesi icerisinde bulunmaktadir ve tum scala kod dosyalarina implicit olarak import edilir. Scala-test'in asser metodu ise Assertions traiti icerisinde tanimlanmistir ve scala'nin default assert metodunun yerine gecer. Ayni sekilde calismasina karsilik, AssertionError yerine TestFailedException firlatir. TestFailedException hatasi cok daha iyi hata mesajlari icerir. Ornek olarak:

assert(left == right)

Gibi bir assertion icin scala'nin assert metodu sadece assertion failed hata mesaji yayinlarken, scala-test'in assert'i 

Error: 2 did not equal 1

Seklinde guzel mesajlar dondurur. Peki bunu nasil yapabiliyor? Biz sadece boolena bir expression pass ediyoruz ama hata mesajinda pass edilen degiskenlerin degerleri de goruntuleniyor. Cunku scala-testin asserti aslinda bir macro. Ve verilen expression'un AST'sini parse ederek hata mesajinda gosterebiliyor. Son bir ornek:

assert(xs.exists(_ == 4))
// Error message: List(1, 2, 3) did not contain 4

Toleransli esitlik
Ayrica scala-test'in 'sister project'i scalactic'in getirdigi triple equals (===) operatoru asertionlar ile birlikte kullanialbilir. Bu operator ile tolerans degeri belirterek yaklasik esitlik assertionlari yapilabilir. Ozellikle kayan nokta aritmetigi gerektiren testlerde yuvarlama hatalarini handle edebilmek icin oldukca kullanisli. Kucuk bir ornek:

import org.scalactic._
import TolerantNumerics._

"Bilmemne bisey" should "soyleydi boyeydi" in {
     implicit val dblEquality = tolerantDoubleEquality(0.01)
     assert(2.00001 === 2.0) // veya 3.0/2 === 1.5
}

2. assertResult : Beklenen ve hesaplanan
Scala-test'in assert macrosu her ne kadar okunabilir olsa da ve guzel hata mesajlari uretse de, assertion uzadigi zaman okunurlugu azalir. Ve de test edilen deger ile beklenen deger arasinda bir ayrim yapamaz. En fazua 2'i, 1'e esit degil der. Ama hangisi hesaplanan hangisi beklenen deger? Bu durumda assert'e alternatif olarak assertResult makrosu kullanilabilir. Beklenilen degeri parantez icinde belirtip, daha sonra suslu parantezler icerisinde de hesaplamayi yapabiliriz. 

val a = 5
val b = 2
assertResult(4) {
    a - b

Bu test fail olacak ve hata mesajinda da tam olarak ne beklendigi ve ne elde edildigi gorulebiliyor olacak:

[info]     Expected 4, but got 2 (MainTest.scala:11)

3. assertThrows: Exception test etme
Bir testtte verilen kodun gerektigi gibi bir hata firlatmasini test ediyor olabiliriz. Bu durumda hangi turde hata firlatilmasini belirterek assertThrows kullanabiliriz:

assertThrows[IndexOutOfBoundsException] {
    metodum(yanlisIndex)
}

Peki ya firlatilan hatayi yakalip, daha yakindan inclemek ve test etmek istersek? Ornegin hata classinin barindirdigi datayi test etmek isteyebiliriz:

val yakalananHata = intercept[IndexOutOfBoundsException] {
    metodum(-1)
}
assert(yakalananHata.getMessage.indexOf("-1") != -1) 

seklinde hata mesajinda metoda pass edilen indexin (ornekte -1) belirtilip belirtilmedigini test edebiliriz. 

Testi pas gecme
Bazi testlerde disa bagimlilik olabilir. Bu bagimliliklar uygun degilse (ornegin bir database'in online olmasi gerekebilrir), test pas gecilebilir. Yanina bir de testin neden pas gectigini belirtecek hata mesaji verebiliriz. 

assume(database.isAlive, "Database gene down :(")
assume(database.getAllUsers.count === 9)

Ipucu verme
Hata mesajlarinda ek olarak yer almasi gereken bilgileri assert'e ve assertResult macrolarina arguman olarak verebiliriz. Bu verilen ek mesajlar, test fail oldugunda hata mesajlarina eklenir.

assert(1 + 1 == 3, "ne bekliyodun gardas")

assertResult(3, "ne bekliyodun gardas") {  1 + 1 }

ve hata mesajinda

[info]     2 did not equal 3 ne bekliyodun gardas (MainTest.scala:11)

seklinde ipuclarini gorebiliriz. Ama assertThrows'da arguman olarak ipucu verilemiyor. Bu durumda withClue yapisi kullanilabilir:

withClue("ne bekliyodun gardas") {
    assertThrows[IndexOutOfBoundsException]{
        metodum(-2)
    }

}

Ozet:
- test stillerine gozattik. Ozellikle FlatSpec ve RefSpec enteresan
- scala-test'in assert macrosu ile sacla'nin default assert'inden daha guzel hata mesajlari urettik
- assertResult macrosu ile beklenen deger ile hesaplanan deger ayridina vardik
- assertThrows ile exception test ettik
- assertionlara ek hata mesajlari ekledik

Bir sonraki testing postunda gorusmek uzere, green testler dilerim. 


Yorumlar

Bu blogdaki popüler yayınlar

Python'da Multithreading ve Multiprocessing

Threat Modeling 1

Encoding / Decoding