سؤال

Edit: going to expose production code on SO! Hope no one steals my secrets!

I have a Controller class for communicating with a device over TCP using the Modbus protocol. I use the NModbus library.

Here are the interfaces the controller class implements:

public interface CoilReader
{
    bool[] Read(ushort startAddress, ushort numberOfCoils);
}

public interface CoilWriter
{
    void WriteSingle(ushort address, bool value);

    void WriteMultiple(ushort startAddress, bool[] values);
}

public interface HoldingRegisterReader
{
    ushort[] Read(ushort startAddress, ushort numberOfRegisters);
}

public interface HoldingRegisterWriter
{
    void WriteSingle(ushort address, ushort value);

    void WriteMultiple(ushort startAddress, ushort[] values);
}

public interface InputReader
{
    bool[] Read(ushort startAddress, ushort numberOfCoils);
}

public interface InputRegisterReader
{
    ushort[] Read(ushort startAddress, ushort numberOfRegisters);
}

public interface ConnectionInfo
{
    string IP { get; set; }
}

Here is the controller class.

using System;
using System.Net.Sockets;
using System.Reflection;
using global::Modbus.Device;

public class Controller
    : ConnectionInfo,
      HoldingRegisterReader,
      InputRegisterReader,
      CoilReader,
      InputReader,
      CoilWriter,
      HoldingRegisterWriter
{
    static Controller()
    {
        AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => loadEmbeddedAssembly(e.Name);
    }

    public virtual string IP
    {
        get
        {
            return this.ip;
        }

        set
        {
            this.ip = value;
        }
    }

    public virtual ushort[] ReadHoldingRegisters(ushort startAddress, ushort numberOfRegisters)
    {
        using (var connection = this.createDeviceConnection())
        {
            return connection.ReadHoldingRegisters(startAddress, numberOfRegisters);
        }
    }

    public virtual ushort[] ReadInputRegisters(ushort startAddress, ushort numberOfRegisters)
    {
        using (var connection = this.createDeviceConnection())
        {
            return connection.ReadInputRegisters(startAddress, numberOfRegisters);
        }
    }

    public virtual bool[] ReadCoils(ushort startAddress, ushort numberOfCoils)
    {
        using (var connection = this.createDeviceConnection())
        {
            return connection.ReadCoils(startAddress, numberOfCoils);
        }
    }

    public virtual bool[] ReadInputs(ushort startAddress, ushort numberOfInputs)
    {
        using (var connection = this.createDeviceConnection())
        {
            return connection.ReadInputs(startAddress, numberOfInputs);
        }
    }

    public virtual void WriteSingleCoil(ushort address, bool value)
    {
        using (var connection = this.createDeviceConnection())
        {
            connection.WriteSingleCoil(address, value);
        }
    }

    public virtual void WriteMultipleCoils(ushort startAddress, bool[] values)
    {
        using (var connection = this.createDeviceConnection())
        {
            connection.WriteMultipleCoils(startAddress, values);
        }
    }

    public virtual void WriteSingleHoldingRegister(ushort address, ushort value)
    {
        using (var connection = this.createDeviceConnection())
        {
            connection.WriteSingleRegister(address, value);
        }
    }

    public virtual void WriteMultipleHoldingRegisters(ushort startAddress, ushort[] values)
    {
        using (var connection = this.createDeviceConnection())
        {
            connection.WriteMultipleRegisters(startAddress, values);
        }
    }

    string ConnectionInfo.IP
    {
        get
        {
            return this.IP;
        }

        set
        {
            this.IP = value;
        }
    }

    ushort[] HoldingRegisterReader.Read(ushort startAddress, ushort numberOfRegisters)
    {
        return this.ReadHoldingRegisters(startAddress, numberOfRegisters);
    }

    ushort[] InputRegisterReader.Read(ushort startAddress, ushort numberOfRegisters)
    {
        return this.ReadInputRegisters(startAddress, numberOfRegisters);
    }

    bool[] CoilReader.Read(ushort startAddress, ushort numberOfCoils)
    {
        return this.ReadCoils(startAddress, numberOfCoils);
    }

    bool[] InputReader.Read(ushort startAddress, ushort numberOfInputs)
    {
        return this.ReadInputs(startAddress, numberOfInputs);
    }

    void CoilWriter.WriteSingle(ushort address, bool value)
    {
        this.WriteSingleCoil(address, value);
    }

    void CoilWriter.WriteMultiple(ushort startAddress, bool[] values)
    {
        this.WriteMultipleCoils(startAddress, values);
    }

    void HoldingRegisterWriter.WriteSingle(ushort address, ushort value)
    {
        this.WriteSingleHoldingRegister(address, value);
    }

    void HoldingRegisterWriter.WriteMultiple(ushort startAddress, ushort[] values)
    {
        this.WriteMultipleHoldingRegisters(startAddress, values);
    }

    private ModbusIpMaster createDeviceConnection()
    {
        const int port = 502;
        var client = new TcpClient();
        client.BeginConnect(this.ip, port, null, null).AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(2));
        if (!client.Connected)
        {
            throw new Exception("Cannot connect to " + this.ip + ":" + port);
        }

        return ModbusIpMaster.CreateIp(client);
    }

    private static Assembly loadEmbeddedAssembly(string name)
    {
        if (name.EndsWith("Retargetable=Yes"))
        {
            return Assembly.Load(new AssemblyName(name));
        }

        var container = Assembly.GetExecutingAssembly();
        var path = new AssemblyName(name).Name + ".dll";

        using (var stream = container.GetManifestResourceStream(path))
        {
            if (stream == null)
            {
                return null;
            }

            var bytes = new byte[stream.Length];
            stream.Read(bytes, 0, bytes.Length);
            return Assembly.Load(bytes);
        }
    }

    private string ip;
}

I do not get the following error from a test created in a Tests project in the same solution as the library that contains this class and its interfaces. However, in a project in a different solution, that consumes the library, I get the following:

------ Test started: Assembly: CareControls.IvisHmi.Tests.dll ------

Unknown .NET Framework Version: v4.5.1
Test 'CareControls.IvisHmi.Tests.Presenters.ModbusTcpTogglePresenterTests.FactMethodName' failed:     FakeItEasy.Core.FakeCreationException : 
Failed to create fake of type "CareControls.Modbus.Tcp.Controller".

Below is a list of reasons for failure per attempted constructor:
  No constructor arguments failed:
    No default constructor was found on the type CareControls.Modbus.Tcp.Controller.

If either the type or constructor is internal, try adding the following attribute to the assembly:
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]


at FakeItEasy.Core.DefaultExceptionThrower.ThrowFailedToGenerateProxyWithResolvedConstructors(Type typeOfFake, String reasonForFailureOfUnspecifiedConstructor, IEnumerable`1 resolvedConstructors)
at FakeItEasy.Creation.FakeObjectCreator.TryCreateFakeWithDummyArgumentsForConstructor(Type typeOfFake, FakeOptions fakeOptions, IDummyValueCreationSession session, String failReasonForDefaultConstructor, Boolean throwOnFailure)
at FakeItEasy.Creation.FakeObjectCreator.CreateFake(Type typeOfFake, FakeOptions fakeOptions, IDummyValueCreationSession session, Boolean throwOnFailure)
at FakeItEasy.Creation.DefaultFakeAndDummyManager.CreateFake(Type typeOfFake, FakeOptions options)
at FakeItEasy.Creation.DefaultFakeCreatorFacade.CreateFake[T](Action`1 options)
at FakeItEasy.A.Fake[T]()
Presenters\ModbusTcpTogglePresenterTests.cs(22,0): at CareControls.IvisHmi.Tests.Presenters.ModbusTcpTogglePresenterTests.FactMethodName()

0 passed, 1 failed, 0 skipped, took 0.93 seconds (xUnit.net 1.9.2 build 1705).

This is the test:

namespace CareControls.IvisHmi.Tests.Presenters
{
    using CareControls.IvisHmi.Framework;
    using CareControls.IvisHmi.Presenters;
    using CareControls.IvisHmi.UI;
    using CareControls.Modbus.Tcp;
    using FakeItEasy;
    using Ploeh.AutoFixture;
    using Xunit;

    public class ModbusTcpTogglePresenterTests
    {
        [Fact]
        public void FactMethodName()
        {
            A.Fake<Controller>();
        }
    }
}

Why does FakeItEasy not think there is a default constructor on the class?

Sorry for the massive post, but it was requested I include code.

Edit: the test passes if I add new Controller() before the A.Fake<Controller>() line:

namespace CareControls.IvisHmi.Tests.Presenters
{
    using CareControls.IvisHmi.Framework;
    using CareControls.IvisHmi.Presenters;
    using CareControls.IvisHmi.UI;
    using CareControls.Modbus.Tcp;
    using FakeItEasy;
    using Ploeh.AutoFixture;
    using Xunit;

    public class ModbusTcpTogglePresenterTests
    {
        [Fact]
        public void FactMethodName()
        {
            new Controller();
            A.Fake<Controller>();
        }
    }
}
هل كانت مفيدة؟

المحلول 4

I solved the problem by turning the createDeviceConnection() method into an NModbusDeviceConnection disposable class. In the static constructor of the NModbusDeviceConnection is where the AssemblyResolve event is subscribed to. Now I can create fakes of Controller without it triggering the Modbus assembly load.

For details, here is the NModbusDeviceConnection class:

// ---------------------------------------------------------------------------------------------------------------------
// <copyright file="NModbusDeviceConnection.cs" company="Care Controls">
//   Copyright (c) Care Controls Inc. All rights reserved.
// </copyright>
// ---------------------------------------------------------------------------------------------------------------------

namespace CareControls.Modbus.Tcp.Internal
{
    using System;
    using System.Net.Sockets;
    using System.Reflection;
    using global::Modbus.Device;

    internal sealed class NModbusDeviceConnection : IDisposable
    {
        static NModbusDeviceConnection()
        {
            AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => loadEmbeddedAssembly(e.Name);
        }

        public NModbusDeviceConnection(string ip)
        {
            const int port = 502;
            var client = new TcpClient();
            client.BeginConnect(ip, port, null, null).AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(2));
            if (!client.Connected)
            {
                throw new Exception("Cannot connect to " + ip + ":" + port);
            }

            this.connection = ModbusIpMaster.CreateIp(client);
        }

        public ushort[] ReadHoldingRegisters(ushort startAddress, ushort numberOfRegisters)
        {
            return this.connection.ReadHoldingRegisters(startAddress, numberOfRegisters);
        }

        public ushort[] ReadInputRegisters(ushort startAddress, ushort numberOfRegisters)
        {
            return this.connection.ReadInputRegisters(startAddress, numberOfRegisters);
        }

        public bool[] ReadCoils(ushort startAddress, ushort numberOfCoils)
        {
            return this.connection.ReadCoils(startAddress, numberOfCoils);
        }

        public bool[] ReadInputs(ushort startAddress, ushort numberOfInputs)
        {
            return this.connection.ReadInputs(startAddress, numberOfInputs);
        }

        public void WriteSingleCoil(ushort address, bool value)
        {
            this.connection.WriteSingleCoil(address, value);
        }

        public void WriteMultipleCoils(ushort startAddress, bool[] values)
        {
            this.connection.WriteMultipleCoils(startAddress, values);
        }

        public void WriteSingleHoldingRegister(ushort address, ushort value)
        {
            this.connection.WriteSingleRegister(address, value);
        }

        public void WriteMultipleHoldingRegisters(ushort address, ushort[] values)
        {
            this.connection.WriteMultipleRegisters(address, values);
        }

        public void Dispose()
        {
            if (this.connection != null)
            {
                this.connection.Dispose();
            }
        }

        private static Assembly loadEmbeddedAssembly(string name)
        {
            if (name.EndsWith("Retargetable=Yes"))
            {
                return Assembly.Load(new AssemblyName(name));
            }

            var container = Assembly.GetExecutingAssembly();
            var path = new AssemblyName(name).Name + ".dll";

            using (var stream = container.GetManifestResourceStream(path))
            {
                if (stream == null)
                {
                    return null;
                }

                var bytes = new byte[stream.Length];
                stream.Read(bytes, 0, bytes.Length);
                return Assembly.Load(bytes);
            }
        }

        private readonly ModbusIpMaster connection;
    }
}

and here is the modified Controller class:

// ---------------------------------------------------------------------------------------------------------------------
// <copyright file="Controller.cs" company="Care Controls">
//   Copyright (c) Care Controls Inc. All rights reserved.
// </copyright>
// ---------------------------------------------------------------------------------------------------------------------

namespace CareControls.Modbus.Tcp
{
    using System;
    using CareControls.Modbus.Tcp.Internal;

    public class Controller
        : ConnectionInfo,
          HoldingRegisterReader,
          InputRegisterReader,
          CoilReader,
          InputReader,
          CoilWriter,
          HoldingRegisterWriter
    {
        public Controller()
        {
            this.newDeviceConnection = () => new NModbusDeviceConnection(this.IP);
        }

        public virtual string IP { get; set; }

        public virtual ushort[] ReadHoldingRegisters(ushort startAddress, ushort numberOfRegisters)
        {
            using (var connection = this.newDeviceConnection())
            {
                return connection.ReadHoldingRegisters(startAddress, numberOfRegisters);
            }
        }

        public virtual ushort[] ReadInputRegisters(ushort startAddress, ushort numberOfRegisters)
        {
            using (var connection = this.newDeviceConnection())
            {
                return connection.ReadInputRegisters(startAddress, numberOfRegisters);
            }
        }

        public virtual bool[] ReadCoils(ushort startAddress, ushort numberOfCoils)
        {
            using (var connection = this.newDeviceConnection())
            {
                return connection.ReadCoils(startAddress, numberOfCoils);
            }
        }

        public virtual bool[] ReadInputs(ushort startAddress, ushort numberOfInputs)
        {
            using (var connection = this.newDeviceConnection())
            {
                return connection.ReadInputs(startAddress, numberOfInputs);
            }
        }

        public virtual void WriteSingleCoil(ushort address, bool value)
        {
            using (var connection = this.newDeviceConnection())
            {
                connection.WriteSingleCoil(address, value);
            }
        }

        public virtual void WriteMultipleCoils(ushort startAddress, bool[] values)
        {
            using (var connection = this.newDeviceConnection())
            {
                connection.WriteMultipleCoils(startAddress, values);
            }
        }

        public virtual void WriteSingleHoldingRegister(ushort address, ushort value)
        {
            using (var connection = this.newDeviceConnection())
            {
                connection.WriteSingleHoldingRegister(address, value);
            }
        }

        public virtual void WriteMultipleHoldingRegisters(ushort startAddress, ushort[] values)
        {
            using (var connection = this.newDeviceConnection())
            {
                connection.WriteMultipleHoldingRegisters(startAddress, values);
            }
        }

        string ConnectionInfo.IP
        {
            get
            {
                return this.IP;
            }

            set
            {
                this.IP = value;
            }
        }

        ushort[] HoldingRegisterReader.Read(ushort startAddress, ushort numberOfRegisters)
        {
            return this.ReadHoldingRegisters(startAddress, numberOfRegisters);
        }

        ushort[] InputRegisterReader.Read(ushort startAddress, ushort numberOfRegisters)
        {
            return this.ReadInputRegisters(startAddress, numberOfRegisters);
        }

        bool[] CoilReader.Read(ushort startAddress, ushort numberOfCoils)
        {
            return this.ReadCoils(startAddress, numberOfCoils);
        }

        bool[] InputReader.Read(ushort startAddress, ushort numberOfInputs)
        {
            return this.ReadInputs(startAddress, numberOfInputs);
        }

        void CoilWriter.WriteSingle(ushort address, bool value)
        {
            this.WriteSingleCoil(address, value);
        }

        void CoilWriter.WriteMultiple(ushort startAddress, bool[] values)
        {
            this.WriteMultipleCoils(startAddress, values);
        }

        void HoldingRegisterWriter.WriteSingle(ushort address, ushort value)
        {
            this.WriteSingleHoldingRegister(address, value);
        }

        void HoldingRegisterWriter.WriteMultiple(ushort startAddress, ushort[] values)
        {
            this.WriteMultipleHoldingRegisters(startAddress, values);
        }

        private readonly Func<NModbusDeviceConnection> newDeviceConnection;
    }
}

نصائح أخرى

According to this:

Faking/mocking an interface gives "no default constructor" error, how can that be?

there is a bug where the wrong exception message can be given. They say that in that case there should still be something causing the exception to be thrown, though.

Things I would suggest:

  • Does adding an explicit default constructor help?
  • Can you successfully instantiate the class normally?
  • Can you successfully instantiate the class with reflection or Activator.CreateInstance?

A static constructor is not the same thing as a public constructor. Are you sure the compiler generates a default public constructor when a static constructor is defined? I wouldn't have thought you'd get both. Does it show up if you use ILDASM to reverse engineer the assembly?

The static constructor may well be called 'lazily' so that when FakeItEasy tries to create a fake by reflecting over the type, the static constructor hasn't yet been called. This would explain why it works when you instantiate the type before creating a fake.

To summarize information gathered by Sam Pearson using FakeItEasy 1.18.0 as well as MSDN documentation the problem arises from an an exception thrown when the Modbus assembly can't be loaded. This is interesting since the Controller's static constructor contains code to deal with this failure, so it must not have been executed. Static constructors are called

… automatically to initialize the class before the first instance is created or any static members are referenced.1

However, FakeItEasy doesn't do either of those things intentionally. It seems that DynamicProxy doesn't either.

My guess is that accessing the class so it can be faked triggers the Modbus assembly load, without triggering the static constructor.

However, it's possible that FakeItEasy's assembly scanning is triggering the assembly load.
This can be checked by adding a test that fakes out something else, like ICollection, and running that instead.
If the error still occurs, then it's the directory scanning that's doing it. In that case, you can use the new Bootstrapper added in FakeItEasy 1.18.0 to disable the directory scanning. We have no official documentation yet, as there's an ongoing discussion about changing the scanning mechanism a little, but I did post about how to disable the scanning of on-disk assemblies. If FakeItEasy's scanning triggers the load, and disabling the scanning defers the assembly load until after the static constructor is called, this may be the easiest thing to do.

Additional possible workarounds:

  1. instantiating the Controller first is one workaround (as noted earlier)
  2. faking one (or all) of the many interfaces that the Controller implements instead of faking the Controller (my personal preference is to fake interfaces, not classes, but sometimes there's a need)
  3. if it's possible, perhaps best would be to address the cause of the failed assembly load, but I bet if that was an option the static constructor wouldn't contain the code that it does.
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top