Sprawdź, czy kod jest uruchomiony w ramach testu jednostkowego

107

Mam test jednostkowy (nUnit). Wiele warstw w dół stosu wywołań metoda zakończy się niepowodzeniem, jeśli zostanie uruchomiona za pośrednictwem testu jednostkowego.

Idealnie byłoby użyć czegoś takiego jak mockowanie do skonfigurowania obiektu, od którego zależy ta metoda, ale jest to kod innej firmy i nie mogę tego zrobić bez dużego nakładu pracy.

Nie chcę konfigurować metod specyficznych dla nUnit - jest tu zbyt wiele poziomów i jest to kiepski sposób wykonywania testów jednostkowych.

Zamiast tego chciałbym dodać coś takiego głęboko w stosie wywołań

#IF DEBUG // Unit tests only included in debug build
if (IsRunningInUnitTest)
   {
   // Do some setup to avoid error
   }
#endif

Więc jakieś pomysły na to, jak napisać IsRunningInUnitTest?

PS Jestem w pełni świadomy, że to nie jest świetny projekt, ale myślę, że jest lepszy niż alternatywy.

Ryan
źródło
5
W teście jednostkowym nie należy bezpośrednio ani pośrednio testować kodu innej firmy. Należy odizolować testowaną metodę od implementacji innej firmy.
Craig Stuntz
15
Tak - zdaję sobie z tego sprawę - w świecie idei, ale czasami musimy być trochę pragmatyczni, nie?
Ryan
9
Wracając do komentarza Craiga - nie jestem pewien, czy to prawda. Jeśli moja metoda opiera się na zachowaniu biblioteki innej firmy w określony sposób, czy nie powinno to być częścią testu? Jeśli aplikacja innej firmy się zmieni, chcę, aby mój test zakończył się niepowodzeniem. Jeśli używasz testów, kpią z tego, jak Twoim zdaniem działa aplikacja innej firmy, a nie jak faktycznie działa.
Ryan
2
Ryan, możesz przetestować założenia dotyczące zachowania osób trzecich, ale to osobny test. Musisz przetestować swój własny kod w izolacji.
Craig Stuntz
2
Rozumiem to, co mówisz, ale poza trywialnym przykładem mówiłbyś o dużej (ogromnej) ilości pracy i nie ma nic, co zapewniłoby, że założenia, które sprawdzasz w teście, są takie same, jak założenia w twoich rzeczywistych metodach . Hmm - debata na temat wpisu na blogu Myślę, że wyślę Ci e-maila, kiedy zbiorę swoje myśli.
Ryan

Odpowiedzi:

80

Robiłem to już wcześniej - musiałem trzymać nos, kiedy to robiłem, ale zrobiłem to. Pragmatyzm za każdym razem bije dogmatyzm. Oczywiście, jeśli nie jest to sposób miły byłaby można go uniknąć, że byłoby świetnie.

Zasadniczo miałem klasę „UnitTestDetector”, która sprawdzała, czy zestaw struktury NUnit został załadowany do bieżącej domeny AppDomain. Wystarczyło to zrobić tylko raz, a następnie zapisać wynik w pamięci podręcznej. Brzydki, ale prosty i skuteczny.

Jon Skeet
źródło
jakaś próbka o UnitTestDetector? i podobne dla MSTest?
Kiquenet,
4
@Kiquenet: Myślę, że po prostu użyję AppDomain.GetAssembliesi sprawdzę odpowiedni zestaw - w przypadku MSTest musisz sprawdzić, które zestawy są załadowane. Spójrz na odpowiedź Ryana na przykład.
Jon Skeet,
To nie jest dla mnie dobre podejście. Wywołuję metodę UnitTest z aplikacji konsolowej i uważa, że ​​jest to aplikacja UnitTest.
Bizhan
1
@Bizhan: Sugerowałbym, że jesteś wtedy w dość specjalistycznej sytuacji i nie powinieneś oczekiwać bardziej ogólnych odpowiedzi. Możesz zadać nowe pytanie ze wszystkimi swoimi specyficznymi wymaganiami. (Jaka jest różnica między na przykład „Twoim kodem wywołującym z aplikacji konsolowej” a „programem do uruchamiania testów”? Jak chcesz odróżnić aplikację konsolową od dowolnego innego narzędzia uruchamiającego testy na konsoli?)
Jon Skeet
75

Biorąc pomysł Jona, oto co wymyśliłem -

using System;
using System.Reflection;

/// <summary>
/// Detect if we are running as part of a nUnit unit test.
/// This is DIRTY and should only be used if absolutely necessary 
/// as its usually a sign of bad design.
/// </summary>    
static class UnitTestDetector
{

    private static bool _runningFromNUnit = false;      

    static UnitTestDetector()
    {
        foreach (Assembly assem in AppDomain.CurrentDomain.GetAssemblies())
        {
            // Can't do something like this as it will load the nUnit assembly
            // if (assem == typeof(NUnit.Framework.Assert))

            if (assem.FullName.ToLowerInvariant().StartsWith("nunit.framework"))
            {
                _runningFromNUnit = true;
                break;
            }
        }
    }

    public static bool IsRunningFromNUnit
    {
        get { return _runningFromNUnit; }
    }
}

Z tyłu, wszyscy jesteśmy na tyle duzi, chłopcy, żeby rozpoznać, kiedy robimy coś, czego prawdopodobnie nie powinniśmy;)

Ryan
źródło
2
+1 Dobra odpowiedź. Można to jednak nieco uprościć, patrz poniżej: stackoverflow.com/a/30356080/184528
cdiggins
Konkretnym projektem, dla którego to napisałem, był (i nadal jest!) .NET 2.0, więc nie ma linq.
Ryan
To użycie działa dla mnie, ale wygląda na to, że nazwa zestawu uległa zmianie od tego czasu. Przeszedłem do rozwiązania Kiquenet za
The_Black_Smurf
Musiałem wyłączyć logowanie do kompilacji
Travis CI,
U mnie działa, muszę hakować błędy .NET core 3 za pomocą brzytwy, które zdarzają się tylko w testach jednostkowych.
jjxtra
62

Na podstawie odpowiedzi Ryana. Ten jest przeznaczony dla platformy testów jednostkowych MS.

Powodem tego jest to, że pokazuję MessageBox przy błędach. Ale moje testy jednostkowe testują również kod obsługi błędów i nie chcę, aby MessageBox pojawiał się podczas uruchamiania testów jednostkowych.

/// <summary>
/// Detects if we are running inside a unit test.
/// </summary>
public static class UnitTestDetector
{
    static UnitTestDetector()
    {
        string testAssemblyName = "Microsoft.VisualStudio.QualityTools.UnitTestFramework";
        UnitTestDetector.IsInUnitTest = AppDomain.CurrentDomain.GetAssemblies()
            .Any(a => a.FullName.StartsWith(testAssemblyName));
    }

    public static bool IsInUnitTest { get; private set; }
}

A oto test jednostkowy:

    [TestMethod]
    public void IsInUnitTest()
    {
        Assert.IsTrue(UnitTestDetector.IsInUnitTest, 
            "Should detect that we are running inside a unit test."); // lol
    }
dan-gph
źródło
8
Mam lepszy sposób, który rozwiązuje problem MessageBox i unieważnia ten hack oraz dostarcza więcej przypadków testów jednostkowych. Używam klasy, która implementuje interfejs, który nazywam ICommonDialogs. Klasa implementacji wyświetla wszystkie wyskakujące okna dialogowe (MessageBox, okna dialogowe plików, próbnik kolorów, okno dialogowe połączenia z bazą danych itp.). Klasy, które muszą wyświetlać pola komunikatów, akceptują ICommonDiaglogs jako parametr konstruktora, który możemy następnie zamokować w teście jednostkowym. Bonus: możesz potwierdzić oczekiwane połączenia MessageBox.
Tony O'Hagan
1
@ Tony, dobry pomysł. To zdecydowanie najlepszy sposób na zrobienie tego. Nie wiem, o czym wtedy myślałem. Myślę, że zastrzyk uzależnienia był dla mnie wtedy jeszcze nowy.
dan-gph
3
Poważnie, ludzie, uczą się o zastrzyku zależności, a po drugie, pozorują przedmioty. Wstrzykiwanie zależności zrewolucjonizuje Twoje programowanie.
dan-gph,
2
Zaimplementuję UnitTestDetector.IsInUnitTest jako „return true”, a test jednostkowy przejdzie. ;) Jedna z tych zabawnych rzeczy, których test jednostkowy wydaje się niemożliwy.
Samer Adra
1
Microsoft.VisualStudio.QualityTools.UnitTestFramework już nie działał. Zmieniono go na Microsoft.VisualStudio.TestPlatform.TestFramework - to znowu działa.
Alexander,
22

Upraszczając rozwiązanie Ryana, możesz po prostu dodać następującą właściwość statyczną do dowolnej klasy:

    public static readonly bool IsRunningFromNUnit = 
        AppDomain.CurrentDomain.GetAssemblies().Any(
            a => a.FullName.ToLowerInvariant().StartsWith("nunit.framework"));
cdiggins
źródło
2
Prawie to samo, co odpowiedź dan-gph (chociaż szukał zestawu narzędzi VS, a nie nunit).
Ryan
19

Stosuję podobne podejście jak tallseth

Jest to podstawowy kod, który można łatwo zmodyfikować, aby uwzględnić buforowanie. Innym dobrym pomysłem byłoby dodanie metody ustawiającej IsRunningInUnitTesti wywołanie UnitTestDetector.IsRunningInUnitTest = falsegłównego punktu wejścia projektu, aby uniknąć wykonywania kodu.

public static class UnitTestDetector
{
    public static readonly HashSet<string> UnitTestAttributes = new HashSet<string> 
    {
        "Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute",
        "NUnit.Framework.TestFixtureAttribute",
    };
    public static bool IsRunningInUnitTest
    {
        get
        {
            foreach (var f in new StackTrace().GetFrames())
                if (f.GetMethod().DeclaringType.GetCustomAttributes(false).Any(x => UnitTestAttributes.Contains(x.GetType().FullName)))
                    return true;
            return false;
        }
    }
}
Jürgen Steinblock
źródło
Takie podejście podoba mi się bardziej niż odpowiedzi, które uzyskały wyższą liczbę głosów. Nie sądzę, aby można było bezpiecznie założyć, że zestawy testów jednostkowych będą ładowane tylko podczas testu jednostkowego, a nazwa procesu może się również różnić w zależności od programisty (np. Niektórzy używają modułu uruchamiającego testy R #).
EM0
To podejście zadziałałoby, ale będzie szukało tych atrybutów za każdym razem, gdy wywoływana jest funkcja pobierająca IsRunningInUnitTest . W niektórych przypadkach może to wpłynąć na wydajność. Sprawdzanie AssemblyName jest tańsze, ponieważ jest wykonywane tylko raz. Pomysł z publicznym ustawiaczem jest dobry, ale w tym przypadku klasa UnitTestDetector powinna zostać umieszczona w zestawie udostępnionym.
Sergey
13

Może przydatne, sprawdzając bieżącą nazwę procesu:

public static bool UnitTestMode
{
    get 
    { 
        string processName = System.Diagnostics.Process.GetCurrentProcess().ProcessName;

        return processName == "VSTestHost"
                || processName.StartsWith("vstest.executionengine") //it can be vstest.executionengine.x86 or vstest.executionengine.x86.clr20
                || processName.StartsWith("QTAgent");   //QTAgent32 or QTAgent32_35
    }
}

I tę funkcję należy również sprawdzić przez unittest:

[TestClass]
public class TestUnittestRunning
{
    [TestMethod]
    public void UnitTestRunningTest()
    {
        Assert.IsTrue(MyTools.UnitTestMode);
    }
}

Źródła:
Matthew Watson w http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/11e68468-c95e-4c43-b02b-7045a52b407e/

Kiquenet
źródło
|| processName.StartsWith("testhost") // testhost.x86dla VS 2019
Kiquenet
9

W trybie testowym Assembly.GetEntryAssembly()wydaje się , że tak null.

#IF DEBUG // Unit tests only included in debug build 
  if (Assembly.GetEntryAssembly() == null)    
  {
    // Do some setup to avoid error    
  }
#endif 

Zauważ, że jeśli Assembly.GetEntryAssembly()tak null, Assembly.GetExecutingAssembly()nie jest.

Dokumentacja mówi: the GetEntryAssemblymetoda może powrócić null, gdy udało montaż został załadowany z niezarządzanych aplikacji.

Eric Bole-Feysot
źródło
8

Gdzieś w testowanym projekcie:

public static class Startup
{
    public static bool IsRunningInUnitTest { get; set; }
}

Gdzieś w projekcie testów jednostkowych:

[TestClass]
public static class AssemblyInitializer
{
    [AssemblyInitialize]
    public static void Initialize(TestContext context)
    {
        Startup.IsRunningInUnitTest = true;
    }
}

Elegancki, nie. Ale prosto i szybko. AssemblyInitializerdotyczy testu MS. Spodziewałbym się, że inne ramy testowe będą miały odpowiedniki.

Edward Brey
źródło
1
Jeśli testowany kod tworzy dodatkowe domeny AppDomains, IsRunningInUnitTestnie jest ustawiany na true w tych AppDomains.
Edward Brey,
Ale można to łatwo rozwiązać, dodając udostępniony zestaw lub deklarując IsRunningInUnitTest w każdej domenie.
Sergey
3

Używam tego tylko do pomijania logiki, która wyłącza wszystkie TraceAppender w log4net podczas uruchamiania, gdy nie jest podłączony debugger. Dzięki temu testy jednostkowe mogą logować się do okna wyników Resharper, nawet jeśli działają w trybie innym niż debugowanie.

Metoda używająca tej funkcji jest wywoływana podczas uruchamiania aplikacji lub podczas uruchamiania urządzenia testowego.

Jest podobny do postu Ryana, ale używa LINQ, odrzuca wymóg System.Reflection, nie buforuje wyniku i jest prywatny, aby zapobiec (przypadkowemu) nadużyciu.

    private static bool IsNUnitRunning()
    {
        return AppDomain.CurrentDomain.GetAssemblies().Any(assembly => assembly.FullName.ToLowerInvariant().StartsWith("nunit.framework"));
    }
Graeme Wicksted
źródło
3

Odniesienie do frameworka nunit nie oznacza, że ​​test faktycznie działa. Na przykład w Unity, gdy aktywujesz testy w trybie odtwarzania, odniesienia nunit są dodawane do projektu. A kiedy uruchamiasz grę, odniesienia istnieją, więc UnitTestDetector nie działałby poprawnie.

Zamiast sprawdzać, czy nunit jest asemblowany, możemy poprosić nunit api o sprawdzenie, czy kod jest w trakcie wykonywania testu, czy nie.

using NUnit.Framework;

// ...

if (TestContext.CurrentContext != null)
{
    // nunit test detected
    // Do some setup to avoid error
}

Edytować:

Uważaj, jeśli jest to wymagane, kontekst testowy może zostać wygenerowany automatycznie .

ChessMax
źródło
2
Proszę nie wrzucaj tutaj kodu. Wyjaśnij, co to robi.
nkr
3

Po prostu użyj tego:

AppDomain.CurrentDomain.IsDefaultAppDomain()

W trybie testowym zwróci fałsz.

shinexyt
źródło
1

Niedawno byłem niezadowolony z tego problemu. Rozwiązałem to w nieco inny sposób. Po pierwsze, nie chciałem zakładać, że framework nunit nigdy nie zostanie załadowany poza środowiskiem testowym; Szczególnie martwiłem się, że programiści uruchamiają aplikację na swoich komputerach. Więc zamiast tego przeszedłem przez stos wywołań. Po drugie, byłem w stanie założyć, że kod testowy nigdy nie zostanie uruchomiony na plikach binarnych wydania, więc upewniłem się, że ten kod nie istnieje w systemie wydania.

internal abstract class TestModeDetector
{
    internal abstract bool RunningInUnitTest();

    internal static TestModeDetector GetInstance()
    {
    #if DEBUG
        return new DebugImplementation();
    #else
        return new ReleaseImplementation();
    #endif
    }

    private class ReleaseImplementation : TestModeDetector
    {
        internal override bool RunningInUnitTest()
        {
            return false;
        }
    }

    private class DebugImplementation : TestModeDetector
    {
        private Mode mode_;

        internal override bool RunningInUnitTest()
        {
            if (mode_ == Mode.Unknown)
            {
                mode_ = DetectMode();
            }

            return mode_ == Mode.Test;
        }

        private Mode DetectMode()
        {
            return HasUnitTestInStack(new StackTrace()) ? Mode.Test : Mode.Regular;
        }

        private static bool HasUnitTestInStack(StackTrace callStack)
        {
            return GetStackFrames(callStack).SelectMany(stackFrame => stackFrame.GetMethod().GetCustomAttributes(false)).Any(NunitAttribute);
        }

        private static IEnumerable<StackFrame> GetStackFrames(StackTrace callStack)
        {
            return callStack.GetFrames() ?? new StackFrame[0];
        }

        private static bool NunitAttribute(object attr)
        {
            var type = attr.GetType();
            if (type.FullName != null)
            {
                return type.FullName.StartsWith("nunit.framework", StringComparison.OrdinalIgnoreCase);
            }
            return false;
        }

        private enum Mode
        {
            Unknown,
            Test,
            Regular
        }
tallseth
źródło
Uważam, że pomysł testowania wersji debugowania podczas wysyłania wersji wydania jest ogólnie złym pomysłem.
Patrick M
1

działa jak marzenie

if (AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.FullName.ToLowerInvariant().StartsWith("nunit.framework")) != null)
{
    fileName = @"C:\Users\blabla\xxx.txt";
}
else
{
    var sfd = new SaveFileDialog
    {     ...     };
    var dialogResult = sfd.ShowDialog();
    if (dialogResult != DialogResult.OK)
        return;
    fileName = sfd.FileName;
}

.

Tom Nobleman
źródło
1

Testy jednostkowe pominą punkt wejścia aplikacji. Przynajmniej dla wpf, winforms i aplikacja konsolowa main()nie są wywoływane.

Jeśli wywoływana jest metoda główna, to jesteśmy w czasie wykonywania , w przeciwnym razie jesteśmy w trybie testu jednostkowego :

public static bool IsUnitTest { get; private set; } = true;

[STAThread]
public static void main()
{
    IsUnitTest = false;
    ...
}
Sinatr
źródło
0

Biorąc pod uwagę, że twój kod jest normalnie uruchamiany w głównym wątku (gui) aplikacji formularzy systemu Windows i chcesz, aby zachowywał się inaczej podczas uruchamiania w teście, możesz sprawdzić

if (SynchronizationContext.Current == null)
{
    // code running in a background thread or from within a unit test
    DoSomething();
}
else
{
    // code running in the main thread or any other thread where
    // a SynchronizationContext has been set with
    // SynchronizationContext.SetSynchronizationContext(synchronizationContext);
    DoSomethingAsync();
}

Używam tego do kodu, który chcę fire and forgotw aplikacji GUI, ale w testach jednostkowych mogę potrzebować obliczonego wyniku dla potwierdzenia i nie chcę zadzierać z wieloma uruchomionymi wątkami.

Działa dla MSTest. Zaletą jest to, że mój kod nie musi sprawdzać samej struktury testowej i jeśli naprawdę potrzebuję zachowania asynchronicznego w pewnym teście, mogę ustawić własny SynchronizationContext.

Należy pamiętać, że nie jest to niezawodna metoda Determine if code is running as part of a unit testzgodnie z żądaniem OP, ponieważ kod może działać wewnątrz wątku, ale w niektórych scenariuszach może to być dobre rozwiązanie (również: jeśli już pracuję z wątku w tle, może to nie być konieczne rozpocząć nowy).

Jürgen Steinblock
źródło
0

Application.Current ma wartość null, gdy działa w ramach testera jednostkowego. Przynajmniej dla mojej aplikacji WPF przy użyciu testera MS Unit. W razie potrzeby to łatwy test. Warto też pamiętać o używaniu Application.Current w kodzie.

Glaucus
źródło
0

Użyłem poniższego w VB w moim kodzie, aby sprawdzić, czy jesteśmy w teście jednostkowym. w szczególności nie chciałem, aby test otwierał Worda

    If Not Application.ProductName.ToLower().Contains("test") then
        ' Do something 
    End If
TomShantisoft
źródło
0

Co powiesz na użycie refleksji i coś takiego:

var underTest = Assembly.GetCallingAssembly ()! = typeof (MainForm) .Assembly;

Zestaw wywołujący będzie tam, gdzie są twoje przypadki testowe i po prostu zastąp MainForm jakiś typ, który jest w testowanym kodzie.

Jon
źródło
-3

Jest też naprawdę proste rozwiązanie, gdy testujesz klasę ...

Po prostu podaj klasę, dla której testujesz, taką właściwość:

// For testing purposes to avoid running certain code in unit tests.
public bool thisIsUnitTest { get; set; }

Teraz twój test jednostkowy może ustawić wartość logiczną „thisIsUnitTest” na true, więc w kodzie, który chcesz pominąć, dodaj:

   if (thisIsUnitTest)
   {
       return;
   } 

Jest to łatwiejsze i szybsze niż przeglądanie zespołów. Przypomina mi Ruby On Rails, w którym chciałbyś sprawdzić, czy jesteś w środowisku TEST.

Danmar Herholdt
źródło
1
Myślę, że zostałeś tutaj odrzucony, ponieważ polegasz na samym teście, aby zmodyfikować zachowanie klasy.
Riegardt Steyn
1
To nie jest gorsze niż wszystkie inne odpowiedzi tutaj.
DonO