Welcome Guest! To enable all features please Login or Register.

Notification

Icon
Error

OneTimeSetUp is called more than once per assembly
Vincent
#1 Posted : Friday, September 1, 2017 2:08:06 PM(UTC)
Rank: Newbie

Groups: Registered
Joined: 9/1/2017(UTC)
Posts: 7
Location: France

Thanks: 2 times
Was thanked: 1 time(s) in 1 post(s)
Per definition: "This attribute is to identify methods that are called once prior to executing any of the tests in a fixture. It may appear on methods of a TestFixture or a SetUpFixture." (https://github.com/nunit/docs/wiki/OneTimeSetUp-Attribute)

My solution contains two assemblies: one for my code, and one for my tests.

My code assembly contains a class containing a static property that can only be set once: it throws an exception if someone tries to set it a second time.

My test assembly contains a OneTimeSetUp method that sets that property. That method should be called only once for all the tests of my assembly. This is not the case: the OneTimeSetUp method is called before each test.

I reproduced this with the latest version of NCrunch (v3.10). Both the built-in TestExplorer of Visual Studio and Resharper behave correctly by calling the method only once.

I don't know how to post the source code, so in the meantime, here it is:

Code project:
Foo.cs

Code:
using System;
using System.Diagnostics;
using System.Threading;

namespace RealCodeAssembly
{
    public class Foo
    {
        public static bool WasCalled { get; set; }
        public static string Initializer { get; set; }

        public Foo()
        {
            var processId = Process.GetCurrentProcess().Id;
            if (WasCalled)
            {
                throw new Exception($"OneTimeSetUp has been already called in the same process {processId} (thread {Thread.CurrentThread.ManagedThreadId})\r\n" + Environment.StackTrace + "\r\nWas:\r\n" + Initializer);
            }
            WasCalled = true;
            Initializer = $"ProcessId = {processId} (thread {Thread.CurrentThread.ManagedThreadId})\r\n" + Environment.StackTrace;
        }
    }
}


(I added some debug code)

Test project
AssemblyLevelClass.cs

Code:
using NUnit.Framework;
using RealCodeAssembly;

[SetUpFixture]
public class AssemblyLevelClass
{
    public Foo Foo { get; set; }

    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        Foo = new Foo();
    }
}


(Please note that the class above isn't in a namespace).

and the test class:

Code:
using System.Collections.Generic;
using System.Threading;
using NUnit.Framework;

namespace TestNinjectOneTimeSetup
{
    [TestFixture]
    public class Class1
    {
        public static IEnumerable<int> DataSource()
        {
            for (int i = 0; i < 1000; ++i)
            {
                yield return i;
            }
        }

        [TestCaseSource(nameof(DataSource))]
        public void FirstTest(int value)
        {
            Thread.Sleep(10);
            Assert.That(value == -1, Is.False);
        }
    }
}


Please note that without the Thread.Sleep, NCrunch behaves correctly. I had to add it to reproduce the bug simply. In my real-life application, there's no Thread.Sleep in the code, but the initialization part is quite big (plenty of Ninject bindings etc.)

I have logs like this one:

OneTimeSetUp: System.Exception : OneTimeSetUp has been already called in the same process 21288 (thread 11)
à System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
à System.Environment.get_StackTrace()
à RealCodeAssembly.Foo..ctor() dans f:\dev\TestNinjectOneTimeSetup\RealCodeAssembly\Foo.cs:ligne 17
à AssemblyLevelClass.OneTimeSetUp() dans f:\dev\TestNinjectOneTimeSetup\TestNinjectOneTimeSetup\AssemblyLevelClass.cs:ligne 12
à System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
à System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
à System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
à NUnit.Framework.Internal.Reflect.InvokeMethod(MethodInfo method, Object fixture, Object[] args)
à NUnit.Framework.Internal.Commands.SetUpTearDownItem.RunSetUp(TestExecutionContext context)
à NUnit.Framework.Internal.Commands.OneTimeSetUpCommand.<>c__DisplayClass0_0.<.ctor>b__0(TestExecutionContext context)
à NUnit.Framework.Internal.Commands.BeforeTestCommand.Execute(TestExecutionContext context)
à NUnit.Framework.Internal.Commands.BeforeTestCommand.Execute(TestExecutionContext context)
à NUnit.Framework.Internal.Execution.CompositeWorkItem.PerformOneTimeSetUp()
à NUnit.Framework.Internal.Execution.CompositeWorkItem.PerformWork()
à NUnit.Framework.Internal.Execution.WorkItem.RunOnCurrentThread()
à NUnit.Framework.Internal.Execution.WorkItem.Execute()
à NUnit.Framework.Internal.Execution.TestWorker.TestWorkerThreadProc()
à System.Threading.ThreadHelper.ThreadStart_Context(Object state)
à System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
à System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
à System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
à System.Threading.ThreadHelper.ThreadStart()
Was:
ProcessId = 21288 (thread 13)
à System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
à System.Environment.get_StackTrace()
à RealCodeAssembly.Foo..ctor() dans f:\dev\TestNinjectOneTimeSetup\RealCodeAssembly\Foo.cs:ligne 20
à AssemblyLevelClass.OneTimeSetUp() dans f:\dev\TestNinjectOneTimeSetup\TestNinjectOneTimeSetup\AssemblyLevelClass.cs:ligne 12
à System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
à System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
à System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
à NUnit.Framework.Internal.Reflect.InvokeMethod(MethodInfo method, Object fixture, Object[] args)
à NUnit.Framework.Internal.Commands.SetUpTearDownItem.RunSetUp(TestExecutionContext context)
à NUnit.Framework.Internal.Commands.OneTimeSetUpCommand.<>c__DisplayClass0_0.<.ctor>b__0(TestExecutionContext context)
à NUnit.Framework.Internal.Commands.BeforeTestCommand.Execute(TestExecutionContext context)
à NUnit.Framework.Internal.Commands.BeforeTestCommand.Execute(TestExecutionContext context)
à NUnit.Framework.Internal.Execution.CompositeWorkItem.PerformOneTimeSetUp()
à NUnit.Framework.Internal.Execution.CompositeWorkItem.PerformWork()
à NUnit.Framework.Internal.Execution.WorkItem.RunOnCurrentThread()
à NUnit.Framework.Internal.Execution.WorkItem.Execute()
à NUnit.Framework.Internal.Execution.TestWorker.TestWorkerThreadProc()
à System.Threading.ThreadHelper.ThreadStart_Context(Object state)
à System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
à System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
à System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
à System.Threading.ThreadHelper.ThreadStart()

The OneTimeSetUp method is called twice in the same process, for the same assembly, in different threads (run sequentially).
Remco
#2 Posted : Saturday, September 2, 2017 12:11:09 AM(UTC)
Rank: NCrunch Developer

Groups: Administrators
Joined: 4/16/2011(UTC)
Posts: 7,145

Thanks: 959 times
Was thanked: 1290 time(s) in 1196 post(s)
Hi, thanks for sharing this post.

This is by design. There's an explanation of the problem here, under Test Runner Re-use.

In short, the mechanic to prevent the OneTimeSetUp code from running more than once only becomes possible if the test process is thrown away between each batch of tests, which causes significant performance problems under continuous testing. This is a trade-off that is unfortunately necessary to support the features that make NCrunch worthwhile. The best solution is to design your code accordingly.

For state inside your tests, you should avoid using static state and use private fields instead.

When working with SetUpFixtures, the solution is to add a static boolean flag that is set when the SetUpFixture is run, then subsequently checked to make sure it is not executed more than once.

If you want to avoid these problems entirely, you can set the test process memory limit setting to 1. This will make NCrunch recycle the test process between each test batch, so it behaves like other runners. Note that to enable intelligent prioritisation of your tests, along with parallel execution and distributed processing, NCrunch needs to create very small test batches, so you'll take a massive performance hit by doing this.
1 user thanked Remco for this useful post.
Vincent on 9/4/2017(UTC)
Vincent
#3 Posted : Monday, September 4, 2017 8:04:50 AM(UTC)
Rank: Newbie

Groups: Registered
Joined: 9/1/2017(UTC)
Posts: 7
Location: France

Thanks: 2 times
Was thanked: 1 time(s) in 1 post(s)
Thanks for the answer.

I read the page you quoted, but wasn't sure it would apply in my case. Maybe the page could be update to refer to the OneTimeSetUp attribute?

I'll use a static field to indicate that the setup has already done. Another option you didn't mention is to add a OneTimeTearDown that undoes the setup, because it's also called each time, after the test.
Remco
#4 Posted : Monday, September 4, 2017 8:09:55 AM(UTC)
Rank: NCrunch Developer

Groups: Administrators
Joined: 4/16/2011(UTC)
Posts: 7,145

Thanks: 959 times
Was thanked: 1290 time(s) in 1196 post(s)
Vincent;11137 wrote:

I read the page you quoted, but wasn't sure it would apply in my case. Maybe the page could be update to refer to the OneTimeSetUp attribute?


Yes, this is a good idea. I'll do that!

Vincent;11137 wrote:

I'll use a static field to indicate that the setup has already done. Another option you didn't mention is to add a OneTimeTearDown that undoes the setup, because it's also called each time, after the test.


It's possible to use OneTimeTearDown, but I generally recommend to engineer tests so that they clear state before they run, rather than after it. This can feel counter intuitive, but making tests clean up after themselves can be risky business with continuous testing. Test runs can die in a range of unexpected ways, and it's quite possible that a test may be unable to perform its cleanup in which case you can end up with sequence-dependent behaviour in the test process.
Users browsing this topic
Guest
Forum Jump  
You cannot post new topics in this forum.
You cannot reply to topics in this forum.
You cannot delete your posts in this forum.
You cannot edit your posts in this forum.
You cannot create polls in this forum.
You cannot vote in polls in this forum.

YAF | YAF © 2003-2011, Yet Another Forum.NET
This page was generated in 0.058 seconds.
Trial NCrunch
Take NCrunch for a spin
Do your fingers a favour and supercharge your testing workflow
Free Download