Hold stø kurs med autotesting!

Unit-tester er nyttig for de fleste systemutviklere.

Noen av oss kjører strikt, metodisk testdrevet utvikling. Andre bruker bare automatiserte tester nå og da som sikkerhetsnett for å unngå regress-feil. Hvor ofte fyrer du selv av testene dine? Kjører du testsuiten din en gang i ny og ne, eller strikt for hver metode du implementerer?

Jeg liker å kjøre testene mine ofte mens jeg arbeider. Det jeg liker enda bedre er å la utviklingsmiljøet mitt kjøre dem for meg, automatisk. Poenget med yrket vårt er jo nettopp å automatisere og effektivisere arbeidsprosesser – dette forsøker jeg å gjøre også med mine egne rutiner og verktøy. La oss se hvordan vi kan få til dette.

Mange av oss bruker allerede Continuous Integration for å oppdage feil i den felles kodebasen. Personlig autotesting er det neste, logiske steget: enhetstestene våre burde kjøre i bakgrunnen hver gang vi endrer noe, og gi oss umiddelbar tilbakemelding når vi knekker funksjonalitet. På denne måten oppdager vi våre egne feil før resten av teamet blir påvirket av dem.

Det finnes flere verktøy som gjør dette for deg:

Det er imidlertid ikke vanskelig å lage et enkelt autotest-verktøy på egen hånd. Jeg synes at det er en god ide å bruke litt tid her og der på å forbedre sitt eget utviklingsmiljø. Vi har jo muligheten til å lage våre egne verktøy (smeden er en av få håndverkere som kan si det samme). Som utvikler bør man iallfall klare å slipe sine egne kniver, for å si det sånn. Og dette er en god anledning!

Et naivt script for autotesting implementerer du på en ettermiddag. Alt du trenger er et lite program som reagerer på endringer i prosjektfilene dine, og kjører testene etter behov.

Algoritmen er enkel: La scriptet kjøre regelmessig, f.eks hvert sekund. Kontroller «forrige endring»-klokkeslettet for alle filene i prosjektet. Dersom klokkeslettet på en eller flere filer har endret seg siden forrige kontroll, så kjør enhetstestene til prosjektet. Analyser output fra testkjøringen, og se hvorvidt noen tester feilet eller kode knakk helt (exceptions). Gi så klar tilbakemelding om testene er ok eller feilet.

Tilbakemeldingen kan være litt grafikk og lyd på desktopen (for eksempel med Growl eller Snarl), en lavalampe på pulten, blinkende LED-lamper på laptopen… vær kreativ! Bare sørg for er at du får en tydelig tilbakemelding på hvorvidt testene feiler eller ikke.

Filmklippet under viser hvordan mitt eget autotest-miljø fungerer. Koden er i dette tilfellet skrevet i et språk som heter Clojure og ser derfor kanskje litt pussig ut, men ikke heng deg opp i det: det som demonstreres er at jeg knekker og fikser tester, og får automatisk godt synlig feedback på dette underveis.

Ikke spesielt komplisert.

«Dette ser jo genialt ut! Hvorfor bruker ikke alle autotest-verktøy allerede?»

Vel, det finnes dessverre noen utfordringer.

Dersom autotesting skal fungere hensiktsmessig må testsuiten som kjøres kun bestå av raske enhetstester, ikke tunge integrasjonstester. Og enhetstestene må gå kjapt! En test-run må ta sekunder, ikke minutter, ellers mister du gevinsten ved den umiddelbare feedbacken.

Problemet med trege tester er betydelig. Det finnes workarounds: man kan analysere avhengigheter mellom tester og kode, kun kjøre de berørte testene, kjøre raske tester først, feilende tester først, nylig feilende tester først… Men mange av oss har dessverre erfart at det er vanskelig å få til et godt fungerende opplegg for dette i et vanlig moderat stort utviklingsprosjekt.

Så er ikke autotesting nyttig likevel, da? Jo! Autotesting fungerer knallbra i små prosjekter (som ikke har samlet på seg så mye kode ennå) og egenopplæring (når du lærer nye verktøy og språk og ønsker å ta små steg med kjapp feedback). Det kan kanskje også fungere i større prosjekter dersom du har muligheten til å segmentere koden og testene dine i mindre moduler på en hensiktsmessig måte.

I bunnen av denne artikkelen har jeg vedlagt et eksempel på hvordan et enkelt autotest-script kan se ut, implementert i Ruby. Scriptet er skrevet for OS X-miljø, men koden bør være relativt lesbar og enkel å porte til ditt eget favorittspråk og utviklingsmiljø.

Happy testing!

Eksempel:

# Command line command which launches all unit tests
RUN_TESTS_CMD = "mvn clean test"

# Pick up any changes in files matching this filepath
FILES_TO_MONITOR = "**/*.java"

# Determine test outcome by scraping test output for #telltale signs of failure or exceptions
def test_failed?(test_output)
  return (test_output["FAIL in"] != nil) # Any 'FAIL in' strings in test result indicates failure
end

def exception_occurred?(test_output)
  return (test_output["EXCEPTION in"] != nil)
end

# Indicate test state by turning the background color of the test terminal
# a different color - yellow red og green. Currently uses Mac AppleScripts, but could be
# an audible signal, lava lamp, or anything else instead.

def indicate_tests_running
  `osascript bin/autotest/make-term-yellow`
  puts "\n\n\n----------------------------------"
  puts "TESTRUN STARTED #{Time.now}"
  puts "----------------------------------\n\n\n"
end  

def indicate_test_success(description)
  `osascript bin/autotest/make-term-green`
  puts description
end  

def indicate_test_errors(description)
  `osascript bin/autotest/make-term-red`
  puts description
end  

$monitored_files = {} # Stores the last changed time of each file    

def files_changed? # Check if any files have been touched/saved since last check
  file_changed = false;  

  Dir[FILES_TO_MONITOR].each do |filepath|
    if(!File.new(filepath).ctime.eql?($monitored_files[filepath]))
      file_changed = true;
      $monitored_files[filepath] = File.new(filepath).ctime
    end
  end  

  return file_changed
end  

def run_test
  indicate_tests_running
  result = `#{RUN_TESTS_CMD}`  #Run the tests, pipe resulting console output back
  display_results(result)
end    

def display_results(testOutput)
  if test_failed?(testOutput)
    indicate_test_errors("SOME TEST(S) FAILED\n"+testOutput)
  elsif exception_occurred?(testOutput)
    indicate_test_errors("EXCEPTIONS OCCURRED\n"+testOutput)
  else
    indicate_test_success("ALL TESTS SUCCEED\n"+testOutput)
  end
end  

def test_loop
  while true
    if files_changed?
      run_test
    end
    sleep(1) #seconds between each poll for file changes
  end
end  

test_loop # Start testing!

3 kommentarer om “Hold stø kurs med autotesting!”

  1. Takk for den gode ideen og en interessant artikkel. En enkel ide, men god. Ser jo at testene fort kan bli for tunge til å kjøre mange ganger i minuttet… men ikke alle prosjekter vokser inn i himmelen.

    Svar
    • Takk for det Sten Morten. Tunge tester er et problem men jeg synes likevel vi kan være mer på hugget etter å gjøre feedback-syklusene våre strammere/kortere i standard JEE-miljøer.

      Svar
  2. Autotesting er *kult*. Det er noen år unna effektiv autotesting for store prosjekter. Spesielt vil prosjekter ha både raske og trege tester. Jeg har fortsatt til gode å se et verktøy for autotesting som håndterer dette bra. Infinitest prioriterer raske tester før trege tester og tester som har nylig feilet foran stabile tester. Men når en treg test først kommer inn i køen «plugger» den igjen systemet. Da tar det lang tid før man får feedback fra den testen man jobbet med, og verdien er borte.

    (Å kjøre integrasjontester som pirker på en database manuelt ved siden av Infinitest er heller ikke spesielt artig)

    Men når denne type problemer har blitt løst, enten ved bedre test-organisering eller ved smartere verktøy, tror jeg alle oppegående utviklere kommer til å ha autotesting som en sentral del av sin verktøykasse. Fem år, topp!

    Svar

Legg igjen en kommentar