-
Notifications
You must be signed in to change notification settings - Fork 21
Non Public Method Replacement
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
-
Home
- QUICK TOUR [SRC]
- FEATURES
- CHEAT SHEET
- PACKAGE MANAGER CONSOLE POWERSHELL REFERENCE
- COMMAND LINE REFERENCE
- APPVEYOR SUPPORT
- MIGRATION
- From Microsoft Research Moles [SRC]
- From Microsoft Fakes [SRC]
- From Telerik JustMock [SRC]
- From Typemock Isolator [SRC]