Laden...

Klasse mit Timer - wie Unittesten?

Erstellt von yngwie vor 11 Jahren Letzter Beitrag vor 10 Jahren 3.120 Views
Y
yngwie Themenstarter:in
238 Beiträge seit 2005
vor 11 Jahren
Klasse mit Timer - wie Unittesten?

Hallo Community,

folgende Ausgangssituation muss Unitgetestet werden:
*Habe eine Kasse "ItemVisitor" mit privater Liste von "Item"-Objekten. *ItemVisitor.AddItem() -> startet einen Verzögerungstimer welcher als Bremse für die Metode "Visit()" dient (siehe unten)
*Nur wenn der Timer abgelaufen ist wird die private Methode "ItemVisitor.Visit()" ausgeführt
*Kommt ein ItemVisitor.AddItem() bevor der Timer abgelaufen ist, wird der Timer verlängert bzw. neu gestartet *Ich soll nach Möglichkeit einen Timer benutzen der im UI-Thread läuft. Hier ist es der System.Windows.Threading.DispatcherTimer.

hier noch mal der Code von der Kasse:


using System;
using System.Linq;
using System.Timers;
using System.Threading;
using System.Diagnostics;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Timer = System.Windows.Threading.DispatcherTimer;

namespace ProductiveCode
{
    public class Item
    {
        public int VisitCount;
    }

    public class ItemVisitor
    {
        private List<Item> items;
        private Timer delayTimer;
        public const int DELAY = 100;

        public ItemVisitor()
        {
            this.delayTimer = new System.Windows.Threading.DispatcherTimer();
            this.delayTimer.Interval = new TimeSpan(0, 0, 0, 0, ItemVisitor.DELAY);
            this.delayTimer.Tick += this.DelayTimerElapsedHandler;
            this.items = new List<Item>();
        }
        
        public void AddItem(Item item)
        {
            Tracer.TraceLine("Enter: ItemVisitor.AddItem()");
            Tracer.TraceLine("Retrigger delayTimer");

            this.items.Add(item);
            this.delayTimer.Stop();
            this.delayTimer.Start();

            Tracer.TraceLine("Exit: ItemVisitor.AddItem()");
        }

        private void DelayTimerElapsedHandler(object sender, EventArgs e)
        {
            Tracer.TraceLine("Enter: ItemVisitor.delayTimer_Elapsed()");
            this.VisitItems();
            Tracer.TraceLine("Exit: ItemVisitor.delayTimer_Elapsed()");
        }

        private void VisitItems()
        {
            Tracer.TraceLine("Enter: ItemVisitor.VisitItems()");

            this.isInVisit = true;
            this.delayTimer.Stop();

            foreach (Item item in this.items)
                item.VisitCount++;

            Tracer.TraceLine("Exit: ItemVisitor.VisitItems()");
        }
    }
}

Wie kann ich durch ein Unittest prüfen ob dieser "Verzögerungfilter" richtig funktioniert?
Mein erster naiver Test sieht so aus:


[TestClass]
namespace Unittests
{
    [TestClass]
    public class ItemVisitorTest
    {
        [TestMethod]
        public void Test_VisitWithDelay()
        {
            ItemVisitor itemVisitor = new ItemVisitor();
            List<Item> temp = new List<Item>();
            for(int i = 0; i < 4; i++)
            {
                Item item = new Item();
                temp.Add(item);
                itemVisitor.AddItem(item);
                Thread.Sleep(ItemVisitor.DELAY / 3);
                temp.ForEach(x => Assert.AreEqual(0, x.VisitCount, "Verzögerung greift nicht -> 'Visit()' gestartet zu früh!"));
            }
            Thread.Sleep(ItemVisitor.Delay * 2);
            temp.ForEach(x => Assert.AreEqual(1, x.VisitCount, "Nicht alle Items besucht, oder zu oft besucht!"));
        }
    }
}

Offensichtlich legt ein Thread.Sleep auch den Timer schlafen(Single-threaded Application), sodass diese Herangehensweise ungeeignet ist. Brauche ich hier einen separaten Thread oder gibt es andere elegante Möglichkeiten?

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo yngwie,

Ich soll nach Möglichkeit einen Timer benutzen der im UI-Thread läuft.

mal eine ungewöhnliche Variante von [FAQ] Warum blockiert mein GUI?, aber letztlich genauso zu lösen, wie alle anderen Fälle von blockierten GUIs.

herbivore

Hinweis von herbivore vor 11 Jahren

Ein Thread, der mit Verweis auf [FAQ] Warum blockiert mein GUI? beantwortet werden kann, wird normalerweise geschlossen. Ich lassen ihn mal offen, für den Fall, dass es in der FAQ für das Szenario Unit-Tests wider Erwarten ungenannte/unberücksichtigte Aspekte gibt. Wenn euch solche einfallen, antwortet bitte hier.

W
872 Beiträge seit 2005
vor 11 Jahren

Dein Test muss nicht testen, daß Framework/DispatcherTimer funktioniert, sondern, dass Deine Methode funktioniert.
Dafür müßtest Du einen Mock für den DispatcherTimer schreiben, der statt des DispatcherTimer aufgerufen wird und in den Asserts musst Du dann prüfen, daß die erwarteten Aufrufe für den Mock stattgefunden sind.

Y
yngwie Themenstarter:in
238 Beiträge seit 2005
vor 11 Jahren

mal eine ungewöhnliche Variante von
>
, aber letztlich genauso zu lösen, wie alle anderen Fälle von blockierten GUIs.

Der Code den ich oben gepostet habe ist nicht "buchstäblich" zu verstehen, es dient als Beispiel für die Problemstellung - wie kann man das Verhalten einer Klasse testen welches von einem Timer getrieben \ beeinflusst wird. Die Synchronization mit dem UI-Thread ist dabei nicht relevant (es sei denn ich verstehe dich gerade nicht richtig).

"Ich soll nach Möglichkeit einen Timer benutzen der im UI-Thread läuft" - ist keine harte Vorgabe. Kann stattdessen auch einen System.Timers.Timer nehmen...

Dein Test muss nicht testen, daß Framework/DispatcherTimer funktioniert, sondern, dass Deine Methode funktioniert.
Dafür müßtest Du einen Mock für den DispatcherTimer schreiben, der statt des DispatcherTimer aufgerufen wird und in den Asserts musst Du dann prüfen, daß die erwarteten Aufrufe für den Mock stattgefunden sind.

Hallo weismat,

für die Methode die vom Timer ausgelöst wird habe ich bereits Unittests geschrieben, das ist nicht das Problem. Was ich sicherstellen will ist dass die Timer-gesteuerte Verzögerung richtig greift. Der Gedanke den Timer zu mocken finde ich interessant. Bin mir allerdings noch nicht ganz sicher wie dieser Trick für meinen Testfall passt... Kannst du es eventuell in Anlehnung an meinen Beispielcode etwas detaillierter erklären? Wie würde meine Testmethode dann aussehen? Etwas Preudocode, keine fertige Lösung.

Gruß

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo yngwie,

Die Synchronization mit dem UI-Thread ist dabei nicht relevant

wenn du einen gui-basierten Timer nimmst, ist - unabhängig von der konkreten Problemstellung - der die in der FAQ beschriebene Synchronisation mit oder besser gesagt Entkopelung von dem GUI-Thread erforderlich.

Wenn du einen nicht gui-basierten Timer verwendest, nicht.

Was ich sicherstellen will ist dass die Timer-gesteuerte Verzögerung richtig greift.

Nicht jedes Verhalten ist einem Unit-Test zugänglich. Klarer wird es vielleicht, wenn du sicherstellen müsstest, dass ein Timer genau einmal Pro Stunde tickt, nicht mehr und nicht weniger. Da müsste dein Unit-Test mindestens eine Stunde laufen, eher mehr, und würde damit die schnelle Ausführbarkeit von Unit-Tests vereiteln.

Aber auch was kürzere Intervalle, kann es sein, dass das nicht in Unit-Tests gehört. Man würde ja auch nicht in einem Unit-Test testen, ob eine E-Mail tatsächlich versendet oder ob ein Dokument tatsächlich (physisch) gedruckt wird, sondern nur, ob die Methode zum Versenden bzw. Drucken aufgerufen wird.

Momentan würde ich mich weismat anschließen und sagen, das Timer-Verhalten muss gar nicht durch einen Unit-Test abgedeckt werden.

herbivore

S
417 Beiträge seit 2008
vor 11 Jahren

Hallo yngwie,

du könntest diesen "Verzögerungstimer" erstmal kapseln und dann unabhängig von deinem ItemVisitor testen, z.B. so:

public class DelayTimer
{
	private System.Timers.Timer timer;
	private Action callback;

	public DelayTimer(Action callback, TimeSpan delay)
	{
		this.callback = callback;
		this.timer = new System.Timers.Timer(delay.Milliseconds);
		this.timer.Elapsed += timer_Elapsed;
	}

	void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
	{
		this.Stop();
		this.callback();
	}

	public void Restart()
	{
		this.Stop();
		this.timer.Start();
	}

	public void Stop()
	{
		this.timer.Stop();
	}
}

Und so könnte dann ein Test aussehen:

[TestMethod]
public void Test_Success()
{
	var count = 0;
	var timer = new DelayTimer(() => ++count, TimeSpan.FromMilliseconds(200));
	timer.Restart();
	Thread.Sleep(100);
	Assert.AreEqual(0, count);

	timer.Restart();
	Thread.Sleep(250);
	Assert.AreEqual(1, count);
}
Y
yngwie Themenstarter:in
238 Beiträge seit 2005
vor 10 Jahren

Nicht jedes Verhalten ist einem Unit-Test zugänglich. Klarer wird es vielleicht, wenn du sicherstellen müsstest, dass ein Timer genau einmal Pro Stunde tickt, nicht mehr und nicht weniger. Da müsste dein Unit-Test mindestens eine Stunde laufen, eher mehr, und würde damit die schnelle Ausführbarkeit von Unit-Tests vereiteln.

Aber auch was kürzere Intervalle, kann es sein, dass das nicht in Unit-Tests gehört. Man würde ja auch nicht in einem Unit-Test testen, ob eine E-Mail tatsächlich versendet oder ob ein Dokument tatsächlich (physisch) gedruckt wird, sondern nur, ob die Methode zum Versenden bzw. Drucken aufgerufen wird.

Momentan würde ich mich weismat anschließen und sagen, das Timer-Verhalten muss gar nicht durch einen Unit-Test abgedeckt werden.

Hallo herbivore, weismat, Sarc

es macht tatsächlich Sinn. Habe nun ein Interface _:::

Das Problem ist gelöst:) Vielen Dank an alle die an der Diskussion teilgenommen haben.