urasandesu / Prig

Prig is a lightweight framework for test indirections in .NET Framework.
Other
117 stars 21 forks source link

There doesn't appear to be a way to pass arguments to constructors of Proxy classes #77

Closed Ben-Pattinson closed 8 years ago

Ben-Pattinson commented 8 years ago

I'm trying to convert from using Fakes in a project I'm working on. On of my tests has a class I need to proxy, but the class takes a parameter in it's constructor. The created proxy class doesn't have this parameter and I can't find any way to provide it. Is this an omission, or have I just missed something?

Thanks

urasandesu commented 8 years ago

The created proxy class doesn't have this parameter

Why does Proxy constructor need the parameter? Could you give me some code snippet?

For your information, a Proxy class is the stub that mocks a specified instance method or property. So, it doesn't have the feature to replace constructor because we cannot determine whether the instance is the target at the construction time.

If you want to replace a constructor, you can do that like this(you should not use a Proxy). I believe that this policy is same as Microsoft Fakes.

Ben-Pattinson commented 8 years ago

Hi, thanks for getting back to me. I wasn't trying to mock the constructor, I wanted to pass parameters into the constructor so that the non-mocked methods would function correctly. In this case it was StreamWriter and I wanted to pass in a MemoryStream so that it would write to that rather than a file. I wanted to mock the dispose method so I could then recover the actually written bytes, and compare them with what should have been written. This works great in Fakes, and I have now replicated it in JustMock, but I couldn't figure out how to do it with Prig. Is there a way to do that?

urasandesu commented 8 years ago

I can't quite understand what you want to do, sorry... As I thought, I will just mock the constructor of StreamWriter if I do the things you said. For example,

Product Code

using System.IO;

namespace Issues77
{
    public class Class1
    {
        public void WriteTwice(string s)
        {
            using (var sw = new StreamWriter(@"C:\hoge.txt"))
            {
                sw.WriteLine(s);
                sw.WriteLine(s);
            }
        }
    }
}

Test Code

using Issues77;
using NUnit.Framework;
using System.IO;
using System.IO.Prig;
using System.Text;
using Urasandesu.Prig.Framework;

namespace Issues77Test
{
    [TestFixture]
    public class Class1Test
    {
        [Test]
        public void WriteTwice_should_write_specified_parameter_twice()
        {
            using (new IndirectionsContext())
            {
                // Arrange
                var actual = new MemoryStream();
                PStreamWriter.ConstructorString().Body =
                    (@this, path) =>
                    {
                        Assert.AreEqual(@"C:\hoge.txt", path);
                        IndirectionsContext.ExecuteOriginal(() =>
                        {
                            var ctor = typeof(StreamWriter).GetConstructor(new[] { typeof(Stream) });
                            ctor.Invoke(@this, new object[] { actual });
                        });
                    };

                // Act
                new Class1().WriteTwice("abc");

                // Assert
                Assert.AreEqual("abc\r\nabc\r\n", Encoding.UTF8.GetString(actual.ToArray()));
            }
        }
    }
}

Incidentally,

I wanted to mock the dispose method ...

I don't have a good feeling about that, because your test code may leak some resource :worried:

Ben-Pattinson commented 8 years ago

Really what I'm trying to do there is capture what would have been written to a file by any methods called directly on streamwriter. The simplest way to do this seemed to be to hook the dispose on a real stream writer attached to a memory stream instead of the file it would normally goto. I can then investigate the memory stream right at the point where it's about to be disposed of. I do this by rewinding the stream and reading it out into a text field. Yes for bonus points, the next question is can I then call the original dispose method to clean everything up nicely?

In the example code there you do successfulyl call the constructor, but I can't see how I'd then hook the dispose.

Thanks

urasandesu commented 8 years ago

Umm..., we have never written the test whether Dispose method is called. Because it can be detected automatically by static code analysis(e.g. Klocwork, Coverity Scan and so on). Test code is also the code that we have to maintain and it requires the cost. So, it should keep to minimum size.

However, you are attempting to tread a thorny path, aren't you? OK, you can hook the Dispose method with Prig as below:

using Issues77;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Prig;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text;
using Urasandesu.Prig.Framework;

namespace Issues77Test
{
    [TestFixture]
    public class Class1Test
    {
        [Test]
        public void WriteTwice_should_write_specified_parameter_twice()
        {
            using (new IndirectionsContext())
            {
                // Arrange
                var mock = new PStreamWriterMock();
                var actual = new MemoryStream();
                mock.SetupConstructorString(@"C:\hoge.txt", actual);

                // Act
                new Class1().WriteTwice("abc");

                // Assert
                mock.VerifyAll();
                Assert.AreEqual("abc\r\nabc\r\n", Encoding.UTF8.GetString(actual.ToArray()));
            }
        }

        [Test]
        public void WriteTwice_should_call_Dispose()
        {
            using (new IndirectionsContext())
            {
                // Arrange
                var mock = new PStreamWriterMock();
                mock.SetupConstructorStringAny();
                mock.SetupDisposeBoolean(true);

                // Act
                new Class1().WriteTwice("abc");

                // Assert
                mock.VerifyAll();
            }
        }

        [Test]
        public void WriteTwice_should_call_Dispose_even_if_an_exception_is_occurred()
        {
            using (new IndirectionsContext())
            {
                // Arrange
                var mock = new PStreamWriterMock();
                mock.SetupConstructorStringAny();
                mock.SetupWriteCharArrayInt32Int32Throws(new IOException());
                mock.SetupDisposeBoolean(true);

                // Act, Assert
                Assert.Throws<IOException>(() => new Class1().WriteTwice("abc"));
                mock.VerifyAll();
            }
        }

        class PStreamWriterMock : PStreamWriter
        {
            string SetupConstructorString_expected_path { get; set; }
            string SetupConstructorString_actual_path { get; set; }

            public void SetupConstructorStringAny()
            {
                SetupConstructorString(null, new MemoryStream(), false);
            }

            public void SetupConstructorString(string expected_path)
            {
                SetupConstructorString(expected_path, new MemoryStream());
            }

            public void SetupConstructorString(string expected_path, MemoryStream substitute, bool addsToVerifyTargets = true)
            {
                SetupConstructorString_expected_path = expected_path;
                ConstructorString().Body =
                    (@this, path) =>
                    {
                        SetupConstructorString_actual_path = path;
                        IndirectionsContext.ExecuteOriginal(() =>
                        {
                            var ctor = typeof(StreamWriter).GetConstructor(new[] { typeof(Stream) });
                            ctor.Invoke(@this, new object[] { substitute });
                        });
                    };
                if (addsToVerifyTargets)
                    m_verifiers.Add(() => Assert.AreEqual(SetupConstructorString_expected_path, SetupConstructorString_actual_path));
            }

            public void SetupWriteCharArrayInt32Int32Throws(Exception exception)
            {
                WriteCharArrayInt32Int32().Body =
                    (@this, buffer, index, count) =>
                    {
                        ExceptionDispatchInfo.Capture(exception).Throw();
                    };
            }

            bool SetupDisposeBoolean_expected_disposing { get; set; }
            bool SetupDisposeBoolean_actual_disposing { get; set; }

            public void SetupDisposeBoolean(bool expected_disposing, bool addsToVerifyTargets = true)
            {
                SetupDisposeBoolean_expected_disposing = expected_disposing;
                DisposeBoolean().Body =
                    (@this, disposing) =>
                    {
                        // Dispose method may be called from its destructor. 
                        // To verify whether the invocation is from really Dispose method, 
                        // you have to check disposing parameter.
                        SetupDisposeBoolean_actual_disposing |= disposing;

                        // You have to execute original Dispose method to avoid some resource leak.
                        IndirectionsContext.ExecuteOriginal(() =>
                        {
                            var dispose = typeof(StreamWriter).GetMethod("Dispose", BindingFlags.Instance | BindingFlags.NonPublic);
                            dispose.Invoke(@this, new object[] { disposing });
                        });
                    };
                if (addsToVerifyTargets)
                    m_verifiers.Add(() => Assert.AreEqual(SetupDisposeBoolean_expected_disposing, SetupDisposeBoolean_actual_disposing));
            }

            List<Action> m_verifiers = new List<Action>();
            public void VerifyAll()
            {
                foreach (var verifier in m_verifiers)
                    verifier();
            }
        }
    }
}

Again, I don't strongly recommend this :sweat_smile:

Ben-Pattinson commented 8 years ago

Yay, that's what I was after, or close enough. The code under test calls dispose on the StreamWriter and the memory stream before execution returns to the test, hence the need to hook Dispose and retrieve the contents of the memory stream just before it's disposed of. Yes it could all be refactored to DI etc, but it's a scary piece of code in production with no tests atm.... so a hands-off approach is much preferred!

Thanks

urasandesu commented 8 years ago

OK. It seems the issue has been resolved, so I will close this issue. Please reopen if there is any problem.

Regards,