Tester for asynkron kode


mandag 12. april 2010 Testing Samtidighet C#

Lenge siden jeg har hatt noe C#-kode på bloggen nå.., la meg gjøre noe med det. På jobben i forrige uke lagde vi to små extension methods jeg har lyst til å dele med verden. Det er ikke noe rocket science, men kan være greit om du har samme behovet som oss, og du ikke har tenkt på det selv.

Systemene vi jobber med inneholder MYE asynkronitet / samtidighet. Når vi skal lage automatiserte tester – spesielt integrasjonstester – må vi forholde oss til at vi ikke øyeblikkelig får det resultatet vi forventer (eventual consistency). En test kan f.eks. sende en melding som plukkes opp og behandles av en annen tråd. Dette fører til endel kompleksitet i testene; ofte legger vi inn en sleep før vi kontrollerer resultatet.

Her er en tenkt test hvor vi har en publisher og en subscriber. Vi publiserer en melding, og kontrollerer at subscriberen har mottatt:

 

2 [TestFixture]
3 public class When_publishing_a_message
4 {
5   [Test]
6   public void Subscriber_should_receive_within_reasonable_time()
7   {
8     When.publisher.distribute(a_message);
9     Thread.Sleep(2_seconds); // 2 sec seems to be the worst case
10     Then.subscriber.ReceivedMessageCount.ShouldEqual(1);
11   }
12
13   private Publisher publisher;
14   private Subscriber subscriber;
15   private Message a_message;
16   private const int 2_seconds = 2000;
17
18   /* Setup, Teardown and BDD snippet not shown */
19 }

Ett av problemene med dette er at faktorer utenfor vår kontroll kan føre til at tiden vi må vente av og til øker ganske mye. Vi bruker da endel tid på å finne den optimale ventetiden, for vi vil ikke at testene skal bruke for lang tid. Men fra tid til annen feiler likevel en og annen test pga. stor belastning på bygg-serveren. Dette er ikke bra – vi må kunne stole på stabiliteten på builden!

For å bedre dette har vi kommet opp med en abstraksjon vi kan bruke i stedet for en hardkodet sleep. Disse extensionmetodene sjekker om et predikat er tilfredstilt helt til det er det, eller til vi har nådd en timeout-verdi:

 

1 public static class AsyncTestExtensions
2 {
3   public static void ShouldBeTrueWithin(
4       this Func<bool> predicate, int milliseconds)
5   {
6     DateTime timeout = DateTime.Now.AddMilliseconds(milliseconds);
7     while (DateTime.Now < timeout)
8     {
9       if (predicate()) return;
10       Thread.Sleep(10);
11     }
12     Assert.Fail();
13   }
14
15   public static void ShouldBeFalseWithin(
16       this Func<bool> predicate, int milliseconds)
17   {
18     ShouldBeTrueWithin(() => !predicate(), milliseconds);
19   }
20 }

Med disse på plass kan vi skive om testen slik:

 

22 [TestFixture]
23 public class When_publishing_a_message
24 {
25   [Test]
26   public void Subscriber_should_receive_within_reasonable_time()
27   {
28     When.publisher.distribute(a_message);
29     Then.subscriber_have_received_a_message.ShouldBeTrueWithin(2_seconds);
30   }
31
32   private Func<bool> subscriber_have_received_a_message = () =>
33     subscriber.ReceivedMessageCount.Equals(1);
34
35   /* Fields, Setup, Teardown and BDD snippet not shown */
36   /* Note : subscriber now needs to be static          */
37 }

Testen er nå mye mere lesbar (i min mening). I tillegg kjører testene gjevnt over raskere, men uten å feile de gangene maskinen trenger litt mere tid på å gjøre seg ferdig.

PS: When og Then-nøkkelordene i testene stammer fra min Ultra-tiny given-when-then DSL-snippet. ShouldEqual-metoden i den første versjonen av testen kommer fra coreTDD, som vi bruker i alle tester i stedet for vanlige nUnit asserts.


comments powered by Disqus