Skip to content

Non Public Method Replacement

Akira Sugiura edited this page Apr 24, 2016 · 9 revisions

Do you like extension methods? It should not be abused, but sometimes it can achieve natural API on the design if it is the function that you want the library to support essentially as its layer. However, whether the library is opened against such extension depends on circumstances. Specifically, note the case that a method having internal class in its signature pertains. This page describes how solve it by Prig!

Let me say that there are DTOs in an existing library:

class ULTableStatus
{
    internal bool IsOpened = false;
    internal int RowsCount = 0;
}

public class ULColumn
{
    public ULColumn(string name)
    {
        Name = name;
    }

    public string Name { get; private set; }
}

public class ULColumns : IEnumerable
{
    ULTableStatus m_status;
    List<ULColumn> m_columns = new List<ULColumn>();

    internal ULColumns(ULTableStatus status)
    {
        m_status = status;
    }

    public void Add(ULColumn column)
    {
        ValidateState(m_status);
        m_columns.Add(column);
    }

    public void Remove(ULColumn column)
    {
        ValidateState(m_status);
        m_columns.Remove(column);
    }

    public IEnumerator GetEnumerator()
    {
        return m_columns.GetEnumerator();
    }

    static void ValidateState(ULTableStatus status)
    {
        if (!status.IsOpened)
            throw new InvalidOperationException("The column can not be modified because owner table has not been opened.");

        if (0 < status.RowsCount)
            throw new ArgumentException("The column can not be modified because some rows already exist.");
    }
}

public class ULTable
{
    ULTableStatus m_status = new ULTableStatus();

    public ULTable(string tableName)
    {
        TableName = tableName;
        Columns = new ULColumns(m_status);
    }

    public string TableName { get; private set; }
    public ULColumns Columns { get; private set; }

    public void Open(string connectionString)
    {
        // connects DB and fills this schema
        ...(snip)...

        // indicates the state being of "Ready"
        m_status.IsOpened = true;
    }
}

I feel bad smell because some classes manage the side-effect "DB Connection" and the state "Data itself of a table" in the class's own. However, the person who created the library may not have motivation to do refactoring for more. He will not be in trouble to test it, because there is an internal class as a buffer for it when viewing only the library - if using InternalsVisibleToAttribute, the class can be accessed unlimitedly.

Well, this library also provides an auto generation tool for the table schema, and it can generate specific columns. Such columns are named like the following convention:

  • <table name> + _ID
    Primary Key
  • DELETED
    Logical Delete Flag
  • CREATED
    Created Date and Time
  • MODIFIED
    Updated Date and Time

OK. So, you probably want to do like "Get auto generated columns only", "Get manually generated columns only" and so on. Unfortunately, the existing library doesn't provide such features, so you decided to create it yourself. In the such case, I think that extension method just fits. Let's write the test:

[TestFixture]
public class ULTableMixinTest
{
    [Test]
    public void GetAutoGeneratedColumns_should_return_columns_that_are_auto_generated()
    {
        // Arrange
        var expected = new[] { new ULColumn("USER_ID"), new ULColumn("DELETED"), new ULColumn("CREATED"), new ULColumn("MODIFIED") };
        var users = new ULTable("USER");
        users.Columns.Add(expected[0]);
        users.Columns.Add(new ULColumn("PASSWORD"));
        users.Columns.Add(new ULColumn("USER_NAME"));
        users.Columns.Add(expected[1]);
        users.Columns.Add(expected[2]);
        users.Columns.Add(expected[3]);


        // Act
        var actual = users.GetAutoGeneratedColumns();


        // Assert
        CollectionAssert.AreEqual(expected, actual);
    }
}

We suppose that there are the columns USER_ID, PASSWORD, USER_NAME, DELETED, CREATED, MODIFIED in the table USER. When invoking the extension method GetAutoGeneratedColumns against the table, we can get the auto generated columns. This test can't even build, so we created minimum implementation to solve it as follows:

public static class ULTableMixin
{
    public static IEnumerable<ULColumn> GetAutoGeneratedColumns(this ULTable @this)
    {
        throw new NotImplementedException();
    }
}

Now, we just run it. It probably throws NotImplementedException, and we will implement the product code temporarily... Huh? What's happened?

Oops! We're caught in a trap!! 😖

Actually, as shown by the stack trace, ULColumns validates whether the columns are modifiable by using the method ValidateState. In the test case, we want to validate the method GetAutoGeneratedColumns, but the exception was thrown in front of it, which is users.Columns.Add(expected[0]);. This is bad...

In this kind of situation, we can remove the unneeded validation temporarily by using Prig. Install Prig, and add the Stub Settings File for the assembly:

Execute the following commands, and copy the Indirection Stub Setting of ULColumns to the clipboard:

PM> Add-Type -Path <full path to ULColumns's assembly. e.g. "C:\Users\User\Prig.Samples\05.NonPublicReplacement\NonPublicReplacement\bin\Debug\NonPublicReplacement.dll">
PM> Find-IndirectionTarget ([<full name to ULColumns. e.g. UntestableLibrary.ULColumns>]) ValidateState | Get-IndirectionStubSetting | Clip

Then, paste it to the Stub Settings File added(e.g. UntestableLibrary.v4.0.30319.v1.0.0.0.prig) and build the solution:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="prig" type="Urasandesu.Prig.Framework.PilotStubberConfiguration.PrigSection, Urasandesu.Prig.Framework" />
  </configSections>
  <prig>
    <stubs>
      <!-- 
          PULColumns.ValidateStateULTableStatus().Body = 
              args => 
              {   // args[0]: UntestableLibrary.ULTableStatus status
                  throw new NotImplementedException();
              };
      -->
      <add name="ValidateStateULTableStatus" alias="ValidateStateULTableStatus">
        <RuntimeMethodInfo xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:x="http://www.w3.org/2001/XMLSchema" z:Id="1" z:FactoryType="MemberInfoSerializationHolder" z:Type="System.Reflection.MemberInfoSerializationHolder" z:Assembly="0" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" xmlns="http://schemas.datacontract.org/2004/07/System.Reflection">
          <Name z:Id="2" z:Type="System.String" z:Assembly="0" xmlns="">ValidateState</Name>
          <AssemblyName z:Id="3" z:Type="System.String" z:Assembly="0" xmlns="">UntestableLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</AssemblyName>
          <ClassName z:Id="4" z:Type="System.String" z:Assembly="0" xmlns="">UntestableLibrary.ULColumns</ClassName>
          <Signature z:Id="5" z:Type="System.String" z:Assembly="0" xmlns="">Void ValidateState(UntestableLibrary.ULTableStatus)</Signature>
          <Signature2 z:Id="6" z:Type="System.String" z:Assembly="0" xmlns="">System.Void ValidateState(UntestableLibrary.ULTableStatus)</Signature2>
          <MemberType z:Id="7" z:Type="System.Int32" z:Assembly="0" xmlns="">8</MemberType>
          <GenericArguments i:nil="true" xmlns="" />
        </RuntimeMethodInfo>
      </add>
    </stubs>
  </prig>
</configuration>

After we finished the above preparation, we can rewrite the test code as the follows:

[Test]
public void GetAutoGeneratedColumns_should_return_columns_that_are_auto_generated()
{
    using (new IndirectionsContext())
    {
        // Arrange
        // Use Prig, and replace the method body to prohibit the validation that depends on side-effect.
        PULColumns.ValidateStateULTableStatus().Body = args => null;

        var expected = new[] { new ULColumn("USER_ID"), new ULColumn("DELETED"), new ULColumn("CREATED"), new ULColumn("MODIFIED") };
        var users = new ULTable("USER");
        users.Columns.Add(expected[0]);
        users.Columns.Add(new ULColumn("PASSWORD"));
        users.Columns.Add(new ULColumn("USER_NAME"));
        users.Columns.Add(expected[1]);
        users.Columns.Add(expected[2]);
        users.Columns.Add(expected[3]);


        // Act
        var actual = users.GetAutoGeneratedColumns();


        // Assert
        CollectionAssert.AreEqual(expected, actual);
    }
}

How does it come out?

Good! This time, we can get the intended result that throws NotImplementedException. As for the rest, we write some code to pass the test, then we do refactoring if all tests are passed, and confirm continued tests success, then add a new test..., which are like a golden cycle, we just repeat the cycle. It's very nice feel, isn't it?

APPENDIX: By the way, do you think that we can test more easily if he design the original library how? In the first place, I think that he should redesign it as the library that is isolated the state from any side-effects. However, the redesign changing their interfaces is probably difficult because it is an existing library. Other way, I suppose that he can do something for it, it is a modification that changes the interfaces from non-public to public.

Please note the following point. If he just publish an interface directly - for example, publishing all fields of ULTableStatus and the constructor .ctor(ULTableStatus) of ULColumns, the library will become unsafe because data inconsistency will be occurred pretty easily. He should publish only interfaces that can keep safety of the library. In this case, I think that there is the following modification:

...(snip)...

public class ULColumns : IEnumerable
{
    List<ULColumn> m_columns = new List<ULColumn>();
    IValidation<ULColumns> m_val;

    public ULColumns(IValidation<ULColumns> val)
    {
        if (val == null)
            throw new ArgumentNullException("val");

        m_val = val;
    }

    public void Add(ULColumn column)
    {
        m_val.Validate(this);
        m_columns.Add(column);
    }

    public void Remove(ULColumn column)
    {
        m_val.Validate(this);
        m_columns.Remove(column);
    }

    ...(snip)...

    internal static IValidation<ULColumns> GetDefaultValidation(ULTableStatus status)
    {
        return new ColumnsVariabilityValidator(status);
    }

    class ColumnsVariabilityValidator : IValidation<ULColumns>
    {
        readonly ULTableStatus m_status;

        public ColumnsVariabilityValidator(ULTableStatus status)
        {
            m_status = status;
        }

        public void Validate(ULColumns t)
        {
            if (!m_status.IsOpened)
                throw new InvalidOperationException("The column can not be modified because owner table has not been opened.");

            if (0 < m_status.RowsCount)
                throw new ArgumentException("The column can not be modified because some rows already exist.");
        }
    }
}

public class ULTable
{
    ULTableStatus m_status = new ULTableStatus();

    public ULTable(string tableName)
    {
        TableName = tableName;
    }

    ...(snip)...

    ULColumns m_columns;
    public virtual ULColumns Columns
    {
        get
        {
            if (m_columns == null)
                m_columns = new ULColumns(ULColumns.GetDefaultValidation(m_status));
            return m_columns;
        }
    }

    ...(snip)...
}

public interface IValidation<T>
{
    void Validate(T obj);
}

I published the constructor of ULColumns, but I didn't specify ULTableStatus directly and I specify the interface IValidation<ULColumns> that have only a method to validate instead of it. I tried outsourcing the function that the method ValidateState that we want to replace by using Prig has. The method accesses the state but doesn't modify it. Therefore, the data consistency of the library is kept continuously if the part is only published. Then, I just virtualized the property creating ULColumns of ULTable.

The test that was difficult to reach the key method if we don't use Prig will become as the follows. We can test easily by using a nomal mocking framework like Moq:

[Test]
public void GetAutoGeneratedColumns_should_return_columns_that_are_auto_generated()
{
    // Arrange
    // Use Moq, and replace the method body to prohibit the validation that depends on side-effect.
    var usersMock = new Mock<ULTable>("USER");
    usersMock.Setup(_ => _.Columns).Returns(new ULColumns(new Mock<IValidation<ULColumns>>().Object));

    var expected = new[] { new ULColumn("USER_ID"), new ULColumn("DELETED"), new ULColumn("CREATED"), new ULColumn("MODIFIED") };
    var users = usersMock.Object;
    users.Columns.Add(expected[0]);
    users.Columns.Add(new ULColumn("PASSWORD"));
    users.Columns.Add(new ULColumn("USER_NAME"));
    users.Columns.Add(expected[1]);
    users.Columns.Add(expected[2]);
    users.Columns.Add(expected[3]);


    // Act
    var actual = users.GetAutoGeneratedColumns();


    // Assert
    CollectionAssert.AreEqual(expected, actual);
}

Complete source code is here