Dynamiczna analiza kodu dla SBT - testy jednostkowe

Wprowadzenie

Dynamiczna analiza programu to analiza oprogramowania komputerowego wykonywanego przez wykonywanie programów na rzeczywistym lub wirtualnym procesorze. Korzystanie z metryk testów, takich jak pokrycie kodu, zapewnia, że przetestowano odpowiednią ilość możliwych zachować programu. Aby analiza dynamiczna programu była skuteczna, program docelowy musi być wykonany z wystarczającą ilością danych wejściowych do testów, aby uzyskać interesujące zachowanie. Testy jednostkowe, testy integracyjne, testy systemowe i testy akceptacyjne wykorzystują dynamiczną analizę programu.

Za wikipedią.

W tym poście skupię się tylko na frameworkach do testów, testach jednostkowych i mierzeniu pokrycia kodu testami.

Frameworki dla testów

  • ScalaTest - jest to chyba najbardziej znany i rozbudowany framework dla testów do języka Scala. Wspiera Scala.js w wersji 0.6.x. Ostatnia wersja snapshot wspiera Scala Native. Posiada osiem różnych stylów pisania testów, jednak autorzy zachęcają do wybrania dwóch dla projektu. Jeden styl dla testów jednostkowo-integracyjnych, drugi - dla akceptacyjnych. Najciekawsze style to:
    • FunSuite i FlatSpec - są to proste, płaskie style pisania testów podobne do JUnit. Jeśli jednak są dla kogoś zbyt awangardowe jest możliwość pisania testów w Scali z użyciem JUnit i assercji z ScalaTest
    • FunSpec, FreeSpec i WordSpec - są to style testów umożliwiające pisanie zagnieżdżonych testów, jednak zagnieżdżenia nie są wymagane (za wyjątkiem stylu FunSpec, gdzie trzeba użyć przynajmniej jeden poziom Describe)
    • FeatureSpec - jest to zaawansowany styl dla pisania testów akceptacyjnych.
  • spec2 - jest to drugi najbardziej znany framework do pisania testów dla języka Scala. Od wersji czwartej wspiera Scala.js w wersji 0.6.x. Ma dwa style pisania testów:
    • org.specs2.mutable.Specificatio - najbardziej przypomina FreeSpec i jest przewidziany do pisania testów jednostkowych i integracyjnych.
    • org.specs2.Specification - styl testów przewidziany do pisania testów akceptacyjnych,
  • uTest - czarny koń tego zestawienia. Wspiera Scala.js w wersji 0.6.x i 1.0.x oraz Scala Native. Autor frameworka skromnie chwali się, że wspiera wszystkie wersje języka Scala. Ma jeden styl pisania testów który wygląda jak FreeTest.
  • MiniTest - stworzony przez Monix do testowania swojej biblioteki. Wspiera Scala.js w wersji 0.6.x. Ma jeden styl pisania testów, który wygląda jak FunSuite.
  • Greenlight - projekt niestety umarł. Wspiera Scala.js w wersji 0.6.x. Ma jeden styl pisania testów, który wygląda jak FlatSpec

Test jednostkowy w uTest

W build.sbt dodajemy uTest do zależności projektu:

  libraryDependencies += "com.lihaoyi" %%% "utest" % "0.6.5" % "test"

i ustawiamy jako framework testowy:

  testFrameworks += new TestFramework("utest.runner.Framework"),

Tworzymy klasę Calculator, którą będziemy testować :

package pl.writeonly.re.shared

class Calculator {
  type T = Int

  def add(a: T, b: T): T = a * b

  def mul(a: T, b: T): T = a + b

  def leq(a: T, b: T): Boolean = a < b

}

Oraz testy dla niej:

package pl.writeonly.re.shared

import utest._

object CalculatorTest extends TestSuite {
  override val tests: Tests = Tests {
    val calculator = new Calculator()
    'addition - {
      val addition: (Int, Int) => Int = (x, y) => calculator.add(x, y)
      "0 + 0 == 0" - {
        assert(addition(0, 0) == 0)
      }
      "2 + 2 == 4" - {
        assert(addition(2, 2) == 4)
      }
    }
    'multiplication - {
      val multiplication: (Int, Int) => Int = (x, y) => calculator.mul(x, y)
      "0 + 0 == 0" - {
        assert(multiplication(0, 0) == 0)
      }
      "2 + 2 == 4" - {
        assert(multiplication(2, 2) == 4)
      }
    }
    'less_or_equal - {
      val less_or_equal: (Int, Int) => Boolean = (x, y) => calculator.leq(x, y)
      "0 <= 2 == true" - {
        assert(less_or_equal(0, 2))
      }
      "2 <= 0 == false" - {
        assert(!less_or_equal(2, 0))
      }
    }
  }
}

Wszystko kompilujemy i uruchamiamy testy za pomocą polecenia:

sbt clean compile test

Testy przeszły, jesteśmy szczęśliwi:

[info] -------------------------------- Running Tests --------------------------------
[info] + pl.writeonly.re.shared.CalculatorTest.addition.0 + 0 == 0 0ms  
[info] + pl.writeonly.re.shared.CalculatorTest.addition.2 + 2 == 4 0ms  
[info] + pl.writeonly.re.shared.CalculatorTest.multiplication.0 + 0 == 0 0ms  
[info] + pl.writeonly.re.shared.CalculatorTest.multiplication.2 + 2 == 4 0ms  
[info] + pl.writeonly.re.shared.CalculatorTest.less_or_equal.0 <= 2 == true 0ms  
[info] + pl.writeonly.re.shared.CalculatorTest.less_or_equal.2 <= 0 == false 0ms  
[info] Tests: 6, Passed: 6, Failed: 0

Testowanie testów - mierzenie pokrycia kodu testami

Skąd mamy mieć pewność, że przetestowaliśmy klasę Calculator w wystarczający sposób? Możemy to częściowo sprawdzić mierząc pokrycie kodu produkcyjnego (tj. klasy Calculator) testami.

Jeśli chodzi o narzędzia do mierzenia pokrycia kodu testami to tutaj król jest jeden i jest nim scoverage. Posiada on wtyczki do:

Przy czym użyjemy tutaj tylko pierwszej z nich.

Dodajemy sbt-scoverage do build.sbt:

addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1")

I wykonujemy:

sbt clean coverage test && sbt coverageReport

gdzie:

  • coverage - wykonuje kompilacje z instrumentacją kodu
  • coverageReport - generuje raport

Niestety powyższe polecenie działa tylko dla implementacji Scala/JVM i Scala.js. Scala Native nie wspiera instrumentacji kodu. Dlatego projekt resentiment trzeba kompilować za pomocą polecenia:

sbt clean compile re/test coverage reJVM/test reJS/test && sbt coverageReport

Teraz możemy otworzyć pliki <folder_projektu>/re/js/target/scala-2.11/scoverage-report/index.html oraz <folder_projektu>/re/jvm/target/scala-2.11/scoverage-report/index.html i zobaczyć, że klasa Calculator ma 100% pokrycia kodu testami. Lider nietechniczny i Product Owner powinni być z nas zadowoleni.

Git-flow i aliasy

Koncepcja Gitflow

Gitflow jest wspaniałą koncepcją pracy z gałęziami w repozytorium Git. Strategia ta jest świetnie opisana na A successful Git branching model.

Jednak początkowo może wydawać się zbyt skomplikowana. Straszy zwłaszcza ilością poleceń, które trzeba wykonać, żeby scalić gałąź z nową funkcjonalnością:

$ git checkout develop
$ git merge --no-ff myfeature
$ git branch -d myfeature
$ git push origin develop

Ilość komend, parametrów i przełączników może powodować niekończące się problemy i dyskusje “czy na pewno potrzebujemy czegoś tak skomplikowanego?”.

Na szczęście istnieje ‘git-flow’.

Narzędzie git-flow

git-flow jest zbiorem rozszerzeń gita dostarczającym wysokopoziomowe operacje na repozytorium, wspierającym strategię rozgałęziania opracowaną przez Vincenta Driessena. Za ściągawka do git-flow

Tutaj już nie mogą toczyć się dyskusje czy powinniśmy scalać z przełącznikiem --no-ff czy bez niego. Możemy wziąć całe narzędzie ustandaryzowane i przetestowane przez społeczność i nie kroimy niczego własnego.

Zalety tego są oczywiste:

  • programista, tester i/lub wdrożeniowiec, który raz nauczył się pracować z Gitflow ma jedną rzecz mniej do nauki przy przenoszeniu się do innego zespołu, gdzie będą stosować dokładnie ten sam Gitflow bez żadnych lokalnych modyfikacji
  • zamiast wpisywania długich wielolinijkowców w konsoli możemy używać pojedynczych komend.

Moje aliasy

Jednak nawet te komendy są dla mnie za długie. Dlatego przygotowałem plik z aliasami Basha

# git flow
git config --global alias.fi 'flow init'
# git flow feature
git config --global alias.ffstart 'flow feature start'
git config --global alias.fffinish 'flow feature finish '
git config --global alias.ffpublish 'flow feature publish'
git config --global alias.ffpull 'flow feature pull origin'
git config --global alias.fftrack 'git flow feature track'
# git flow release
git config --global alias.frstart 'flow release start'
git config --global alias.frpusblish 'flow release publish'
git config --global alias.frfinish 'flow release finish'
# git flow hotfix
git config --global alias.fhstart 'flow hotfix start'
git config --global alias.fhfinish 'flow hotfix finish'

Można go zastosować poleceniem

curl -s https://raw.githubusercontent.com/writeonly/cli/master/git_config.sh | bash

i cieszyć się krótkimi komendami jak

git ffstart myfeature
git fffinish myfeature
Follow