diff --git a/Documentation/Tutorial.md b/Documentation/Tutorial.md index 829c512..ff5e9a2 100644 --- a/Documentation/Tutorial.md +++ b/Documentation/Tutorial.md @@ -105,6 +105,28 @@ var stub = new StubIPhoneBook() .MyNumber_Set(value => newNumber = value); ``` +## Stubbing indexers +```csharp +var stub = new StubIGenericContainer(); + +// stubbing indexer getter +stub.Item_Get(index => +{ + // we're expecting the code under test to get index 5 + if (index != 5) throw new IndexOutOfRangeException(); + return 99; +}); + +// stubbing indexer setter +int res = -1; +stub.Item_Set((index, value) => +{ + // we're expecting the code under test to only set index 7 + if (index != 7) throw new IndexOutOfRangeException(); + res = value; +}); +``` + ## Stubbing events ```csharp diff --git a/NuGet/SimpleStubs.nuspec b/NuGet/SimpleStubs.nuspec index 91a9b97..a94119c 100644 --- a/NuGet/SimpleStubs.nuspec +++ b/NuGet/SimpleStubs.nuspec @@ -2,7 +2,7 @@ Etg.SimpleStubs - 2.2.0 + 2.3.0 SimpleStubs mocking framework Microsoft Studios (BigPark) Microsoft Studios (BigPark) diff --git a/src/SimpleStubs.CodeGen/CodeGen/IPropertyStubber.cs b/src/SimpleStubs.CodeGen/CodeGen/IPropertyStubber.cs new file mode 100644 index 0000000..3c99759 --- /dev/null +++ b/src/SimpleStubs.CodeGen/CodeGen/IPropertyStubber.cs @@ -0,0 +1,11 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Etg.SimpleStubs.CodeGen +{ + internal interface IPropertyStubber + { + ClassDeclarationSyntax StubProperty(ClassDeclarationSyntax classDclr, IPropertySymbol propertySymbol, + INamedTypeSymbol stubbedInterface); + } +} \ No newline at end of file diff --git a/src/SimpleStubs.CodeGen/CodeGen/InterfaceStubber.cs b/src/SimpleStubs.CodeGen/CodeGen/InterfaceStubber.cs index ca4d4a0..52b62aa 100644 --- a/src/SimpleStubs.CodeGen/CodeGen/InterfaceStubber.cs +++ b/src/SimpleStubs.CodeGen/CodeGen/InterfaceStubber.cs @@ -12,9 +12,11 @@ namespace Etg.SimpleStubs.CodeGen internal class InterfaceStubber : IInterfaceStubber { private readonly IEnumerable _methodStubbers; + private readonly IEnumerable _propertyStubbers; - public InterfaceStubber(IEnumerable methodStubbers) + public InterfaceStubber(IEnumerable methodStubbers, IEnumerable propertyStubbers) { + _propertyStubbers = propertyStubbers; _methodStubbers = new List(methodStubbers); } @@ -32,6 +34,7 @@ public CompilationUnitSyntax StubInterface(CompilationUnitSyntax cu, InterfaceDe classDclr = RoslynUtils.CopyGenericConstraints(interfaceType, classDclr); classDclr = AddStubContainerField(classDclr, stubName); + classDclr = StubProperties(interfaceType, classDclr); classDclr = StubMethods(interfaceType, classDclr); string fullNameSpace = semanticModel.GetDeclaredSymbol(namespaceNode).ToString(); @@ -42,9 +45,22 @@ public CompilationUnitSyntax StubInterface(CompilationUnitSyntax cu, InterfaceDe return cu; } + private ClassDeclarationSyntax StubProperties(INamedTypeSymbol interfaceType, ClassDeclarationSyntax classDclr) + { + IEnumerable propertiesToStub = RoslynUtils.GetAllMembers(interfaceType); + foreach (IPropertySymbol propertySymbol in propertiesToStub) + { + foreach (IPropertyStubber propertyStubber in _propertyStubbers) + { + classDclr = propertyStubber.StubProperty(classDclr, propertySymbol, interfaceType); + } + } + return classDclr; + } + private ClassDeclarationSyntax StubMethods(INamedTypeSymbol interfaceType, ClassDeclarationSyntax classDclr) { - List methodsToStub = RoslynUtils.GetAllMethods(interfaceType); + IEnumerable methodsToStub = RoslynUtils.GetAllMembers(interfaceType); foreach (IMethodSymbol methodSymbol in methodsToStub) { foreach (IMethodStubber methodStubber in _methodStubbers) diff --git a/src/SimpleStubs.CodeGen/CodeGen/OrdinaryMethodStubber.cs b/src/SimpleStubs.CodeGen/CodeGen/OrdinaryMethodStubber.cs index 618e2ba..859bd94 100644 --- a/src/SimpleStubs.CodeGen/CodeGen/OrdinaryMethodStubber.cs +++ b/src/SimpleStubs.CodeGen/CodeGen/OrdinaryMethodStubber.cs @@ -27,7 +27,7 @@ public ClassDeclarationSyntax StubMethod(ClassDeclarationSyntax classDclr, IMeth SF.IdentifierName(methodSymbol.GetContainingInterfaceGenericQualifiedName()))); string delegateTypeName = NamingUtils.GetDelegateTypeName(methodSymbol, stubbedInterface); - string parameters = FormatParameters(methodSymbol); + string parameters = StubbingUtils.FormatParameters(methodSymbol); string callDelegateStmt = StubbingUtils.GenerateInvokeDelegateStmt(delegateTypeName, methodSymbol.GetGenericName(), parameters); if (!methodSymbol.ReturnsVoid) @@ -40,21 +40,5 @@ public ClassDeclarationSyntax StubMethod(ClassDeclarationSyntax classDclr, IMeth return classDclr; } - - private static string FormatParameters(IMethodSymbol methodSymbol) - { - return string.Join(", ", methodSymbol.Parameters.Select(p => - { - if (p.RefKind == RefKind.Out) - { - return $"out {p.Name}"; - } - if (p.RefKind == RefKind.Ref) - { - return $"ref {p.Name}"; - } - return p.Name; - })); - } } } \ No newline at end of file diff --git a/src/SimpleStubs.CodeGen/CodeGen/PropertyStubber.cs b/src/SimpleStubs.CodeGen/CodeGen/PropertyStubber.cs index bd17ecd..cd7442f 100644 --- a/src/SimpleStubs.CodeGen/CodeGen/PropertyStubber.cs +++ b/src/SimpleStubs.CodeGen/CodeGen/PropertyStubber.cs @@ -1,79 +1,75 @@ -using System; +using Etg.SimpleStubs.CodeGen.Utils; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Etg.SimpleStubs.CodeGen.Utils; namespace Etg.SimpleStubs.CodeGen { - using Microsoft.CodeAnalysis.CSharp; - using System.Linq; - using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + using SF = SyntaxFactory; - internal class PropertyStubber : IMethodStubber + internal class PropertyStubber : IPropertyStubber { - public ClassDeclarationSyntax StubMethod(ClassDeclarationSyntax classDclr, IMethodSymbol methodSymbol, + public ClassDeclarationSyntax StubProperty(ClassDeclarationSyntax classDclr, IPropertySymbol propertySymbol, INamedTypeSymbol stubbedInterface) { - if (!methodSymbol.IsPropertyAccessor()) - { - return classDclr; - } - - string delegateTypeName = NamingUtils.GetDelegateTypeName(methodSymbol, stubbedInterface); + string indexerType = propertySymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + BasePropertyDeclarationSyntax propDclr = null; - string propName = methodSymbol.AssociatedSymbol.Name; - string propType = - ((IPropertySymbol) methodSymbol.AssociatedSymbol).Type.ToDisplayString( - SymbolDisplayFormat.FullyQualifiedFormat); - var propDclr = GetPropDclr(classDclr, propName); - if (propDclr == null) + if (propertySymbol.GetMethod != null) { - propDclr = SF.PropertyDeclaration(SF.ParseTypeName(propType), SF.Identifier(propName)) - .WithExplicitInterfaceSpecifier(SF.ExplicitInterfaceSpecifier( - SF.IdentifierName(methodSymbol.GetContainingInterfaceGenericQualifiedName()))); - } + IMethodSymbol getMethodSymbol = propertySymbol.GetMethod; + string parameters = StubbingUtils.FormatParameters(getMethodSymbol); - if (methodSymbol.IsPropertyGetter()) - { + string delegateTypeName = NamingUtils.GetDelegateTypeName(getMethodSymbol, stubbedInterface); var accessorDclr = SF.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, SF.Block( SF.List(new[] { - SF.ParseStatement("return " + StubbingUtils.GenerateInvokeDelegateStmt(delegateTypeName, methodSymbol.Name, "")) + SF.ParseStatement("return " + StubbingUtils.GenerateInvokeDelegateStmt(delegateTypeName, getMethodSymbol.Name, parameters)) }))); + + propDclr = CreatePropertyDclr(getMethodSymbol, indexerType); propDclr = propDclr.AddAccessorListAccessors(accessorDclr); + } - else if (methodSymbol.IsPropertySetter()) + if (propertySymbol.SetMethod != null) { + IMethodSymbol setMethodSymbol = propertySymbol.SetMethod; + string parameters = $"{StubbingUtils.FormatParameters(setMethodSymbol)}"; + string delegateTypeName = NamingUtils.GetDelegateTypeName(setMethodSymbol, stubbedInterface); var accessorDclr = SF.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, SF.Block( SF.List(new[] { - SF.ParseStatement(StubbingUtils.GenerateInvokeDelegateStmt(delegateTypeName, methodSymbol.Name, "value")) + SF.ParseStatement(StubbingUtils.GenerateInvokeDelegateStmt(delegateTypeName, setMethodSymbol.Name, parameters)) }))); - propDclr = propDclr.AddAccessorListAccessors(accessorDclr); - } - - if (propDclr != null) - { - PropertyDeclarationSyntax existingPropDclr = GetPropDclr(classDclr, propName); - if (existingPropDclr != null) + if (propDclr == null) { - classDclr = classDclr.ReplaceNode(existingPropDclr, propDclr); - } - else - { - classDclr = classDclr.AddMembers(propDclr); + propDclr = CreatePropertyDclr(setMethodSymbol, indexerType); } + propDclr = propDclr.AddAccessorListAccessors(accessorDclr); } + classDclr = classDclr.AddMembers(propDclr); return classDclr; } - private static PropertyDeclarationSyntax GetPropDclr(ClassDeclarationSyntax classDclr, string propName) + private BasePropertyDeclarationSyntax CreatePropertyDclr(IMethodSymbol methodSymbol, string propType) { - return - classDclr.DescendantNodes() - .OfType() - .FirstOrDefault(p => p.Identifier.Text == propName); + if (methodSymbol.IsIndexerAccessor()) + { + IndexerDeclarationSyntax indexerDclr = SF.IndexerDeclaration( + SF.ParseTypeName(propType)) + .WithExplicitInterfaceSpecifier(SF.ExplicitInterfaceSpecifier( + SF.IdentifierName(methodSymbol.GetContainingInterfaceGenericQualifiedName()))); + indexerDclr = indexerDclr.AddParameterListParameters( + RoslynUtils.GetMethodParameterSyntaxList(methodSymbol).ToArray()); + return indexerDclr; + } + + string propName = methodSymbol.AssociatedSymbol.Name; + PropertyDeclarationSyntax propDclr = SF.PropertyDeclaration(SF.ParseTypeName(propType), SF.Identifier(propName)) + .WithExplicitInterfaceSpecifier(SF.ExplicitInterfaceSpecifier( + SF.IdentifierName(methodSymbol.GetContainingInterfaceGenericQualifiedName()))); + return propDclr; } } } \ No newline at end of file diff --git a/src/SimpleStubs.CodeGen/DI/DIModule.cs b/src/SimpleStubs.CodeGen/DI/DIModule.cs index b1de872..3183422 100644 --- a/src/SimpleStubs.CodeGen/DI/DIModule.cs +++ b/src/SimpleStubs.CodeGen/DI/DIModule.cs @@ -30,8 +30,11 @@ public static ContainerBuilder RegisterTypes(string testProjectPath, string stub { new OrdinaryMethodStubber(), new EventStubber(), - new PropertyStubber(), new StubbingDelegateGenerator() + }, + new IPropertyStubber[] + { + new PropertyStubber() }); return interfaceStubber; }).As().SingleInstance(); diff --git a/src/SimpleStubs.CodeGen/SimpleStubs.CodeGen.csproj b/src/SimpleStubs.CodeGen/SimpleStubs.CodeGen.csproj index c501226..0e5bf5d 100644 --- a/src/SimpleStubs.CodeGen/SimpleStubs.CodeGen.csproj +++ b/src/SimpleStubs.CodeGen/SimpleStubs.CodeGen.csproj @@ -102,6 +102,7 @@ + diff --git a/src/SimpleStubs.CodeGen/Utils/NamingUtils.cs b/src/SimpleStubs.CodeGen/Utils/NamingUtils.cs index adfc2eb..ae5cc4c 100644 --- a/src/SimpleStubs.CodeGen/Utils/NamingUtils.cs +++ b/src/SimpleStubs.CodeGen/Utils/NamingUtils.cs @@ -42,7 +42,7 @@ public static string GetDelegateTypeName(IMethodSymbol methodSymbol, INamedTypeS methodName = SerializeName(methodSymbol.ContainingSymbol) + "_" + methodName; } - if (methodSymbol.IsOrdinaryMethod()) + if (methodSymbol.IsOrdinaryMethod() || methodSymbol.IsIndexerAccessor()) { if (methodSymbol.Parameters.Any()) { diff --git a/src/SimpleStubs.CodeGen/Utils/RoslynExtensions.cs b/src/SimpleStubs.CodeGen/Utils/RoslynExtensions.cs index 75e10f1..21c0ca8 100644 --- a/src/SimpleStubs.CodeGen/Utils/RoslynExtensions.cs +++ b/src/SimpleStubs.CodeGen/Utils/RoslynExtensions.cs @@ -42,6 +42,22 @@ public static bool IsPropertyGetter(this IMethodSymbol methodSymbol) return methodSymbol.MethodKind == MethodKind.PropertyGet; } + public static bool IsIndexerGetter(this IMethodSymbol methodSymbol) + { + return methodSymbol.Name == "get_Item"; + } + + public static bool IsIndexerSetter(this IMethodSymbol methodSymbol) + { + return methodSymbol.Name == "set_Item"; + } + + public static bool IsIndexerAccessor(this IMethodSymbol methodSymbol) + { + IPropertySymbol propertySymbol = methodSymbol.AssociatedSymbol as IPropertySymbol; + return propertySymbol != null && propertySymbol.IsIndexer; + } + public static string GetGenericName(this IMethodSymbol methodSymbol) { string name = methodSymbol.Name; @@ -52,7 +68,7 @@ public static string GetGenericName(this IMethodSymbol methodSymbol) return name; } - public static string GetContainingInterfaceGenericQualifiedName(this IMethodSymbol methodSymbol) + public static string GetContainingInterfaceGenericQualifiedName(this ISymbol methodSymbol) { return methodSymbol.ContainingSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); } @@ -102,5 +118,21 @@ public static bool IsInternal(this TypeDeclarationSyntax typeDclr) }; return !typeDclr.Modifiers.Any(modifier => nonInternalModifiers.Contains(modifier.RawKind)); } + + public static BasePropertyDeclarationSyntax AddAccessorListAccessors(this BasePropertyDeclarationSyntax baseDclr, params AccessorDeclarationSyntax[] accessors) + { + var propDclr = baseDclr as PropertyDeclarationSyntax; + if (propDclr != null) + { + return propDclr.AddAccessorListAccessors(accessors); + } + + var indexerDclr = baseDclr as IndexerDeclarationSyntax; + if (indexerDclr != null) + { + return indexerDclr.AddAccessorListAccessors(accessors); + } + throw new InvalidOperationException(); + } } } \ No newline at end of file diff --git a/src/SimpleStubs.CodeGen/Utils/RoslynUtils.cs b/src/SimpleStubs.CodeGen/Utils/RoslynUtils.cs index 514a836..cdfd12e 100644 --- a/src/SimpleStubs.CodeGen/Utils/RoslynUtils.cs +++ b/src/SimpleStubs.CodeGen/Utils/RoslynUtils.cs @@ -55,21 +55,21 @@ public static List GetMethodParameterSyntaxList(IMethodSymbol m return paramsSyntaxList; } - public static List GetAllMethods(INamedTypeSymbol interfaceType) + public static List GetAllMembers(INamedTypeSymbol interfaceType) { - var methodsToStub = new List(interfaceType.GetMembers().OfType()); - methodsToStub.AddRange(GetAllInheritedMethods(interfaceType)); + var methodsToStub = new List(interfaceType.GetMembers().OfType()); + methodsToStub.AddRange(GetAllInheritedMethods(interfaceType)); return methodsToStub; } - public static IEnumerable GetAllInheritedMethods(ITypeSymbol typeSymbol) + public static IEnumerable GetAllInheritedMethods(ITypeSymbol typeSymbol) { - var methods = new List(); + var methods = new List(); if (typeSymbol.AllInterfaces.Any()) { foreach (var baseInterfaceType in typeSymbol.AllInterfaces) { - methods.AddRange(baseInterfaceType.GetMembers().OfType()); + methods.AddRange(baseInterfaceType.GetMembers().OfType()); } } diff --git a/src/SimpleStubs.CodeGen/Utils/StubbingUtils.cs b/src/SimpleStubs.CodeGen/Utils/StubbingUtils.cs index 8c0d8e9..c29daa7 100644 --- a/src/SimpleStubs.CodeGen/Utils/StubbingUtils.cs +++ b/src/SimpleStubs.CodeGen/Utils/StubbingUtils.cs @@ -9,5 +9,21 @@ public static string GenerateInvokeDelegateStmt(string delegateTypeName, string { return $"_stubs.GetMethodStub<{delegateTypeName}>(\"{methodName}\").Invoke({parameters});\n"; } - } + + public static string FormatParameters(IMethodSymbol methodSymbol) + { + return string.Join(", ", methodSymbol.Parameters.Select(p => + { + if (p.RefKind == RefKind.Out) + { + return $"out {p.Name}"; + } + if (p.RefKind == RefKind.Ref) + { + return $"ref {p.Name}"; + } + return p.Name; + })); + } + } } \ No newline at end of file diff --git a/test/TestClassLibrary/ITestInterface.cs b/test/TestClassLibrary/ITestInterface.cs index 96a05be..efc525f 100644 --- a/test/TestClassLibrary/ITestInterface.cs +++ b/test/TestClassLibrary/ITestInterface.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; namespace TestClassLibrary @@ -20,12 +21,18 @@ public interface IPhoneBook event EventHandler PhoneNumberChanged; } + // just to make sure all inherited members are stubbed + public interface IPhoneBookSpecial : IPhoneBook + { + } + public interface IContainer { T GetElement(int index); void SetElement(int index, T value); bool GetElement(int index, out object value); + } public interface IRefUtils @@ -74,6 +81,18 @@ internal interface IInternalInterface T GetX(); } + public interface IGenericContainer + { + T this[int index] { get; set; } + + T this[string key, int n] { get; } + } + + // just to make sure inherited indexers are stubbed + public interface IGenericContainerSubInterface : IGenericContainer + { + } + public interface IInterfaceWithGenericMethod { T GetFoo() where T : class; diff --git a/test/TestClassLibraryTest/UnitTest.cs b/test/TestClassLibraryTest/UnitTest.cs index b948b7e..506697a 100644 --- a/test/TestClassLibraryTest/UnitTest.cs +++ b/test/TestClassLibraryTest/UnitTest.cs @@ -102,7 +102,7 @@ public void TestThatExceptionIsThrownWhenStubIsNotSetup() [TestMethod] [ExpectedException(typeof(SimpleStubsException))] - + public void TestThatExceptionIsThrownWhenMethodIsCalledMoreThanExpected() { var stub = new StubIPhoneBook().GetContactPhoneNumber((p1, p2) => 12345678, Times.Once); @@ -115,7 +115,7 @@ public void TestThatExceptionIsThrownWhenMethodIsCalledMoreThanExpected() public void TestThatMethodStubCanBeOverwritten() { var stub = new StubIPhoneBook().GetContactPhoneNumber((p1, p2) => 12345678); - stub.GetContactPhoneNumber((p1, p2) => 11122233, overwrite:true); + stub.GetContactPhoneNumber((p1, p2) => 11122233, overwrite: true); IPhoneBook phoneBook = stub; Assert.AreEqual(11122233, phoneBook.GetContactPhoneNumber("John", "Smith")); @@ -165,12 +165,63 @@ public void TestRefParameter() int i1 = 1; int i2 = 2; - ((IRefUtils) stub).Swap(ref i1, ref i2); + ((IRefUtils)stub).Swap(ref i1, ref i2); Assert.AreEqual(2, i1); Assert.AreEqual(1, i2); } + [TestMethod] + public void TestIndexerGet() + { + var stub = new StubIGenericContainer(); + stub.Item_Get(index => + { + switch (index) + { + case 0: + return 13; + case 1: + return 5; + default: + throw new IndexOutOfRangeException(); + } + }); + + IGenericContainer container = stub; + Assert.AreEqual(13, container[0]); + Assert.AreEqual(5, container[1]); + } + + [TestMethod] + public void TestIndexerSet() + { + var stub = new StubIGenericContainer(); + int res = -1; + stub.Item_Set((index, value) => + { + if (index != 0) throw new IndexOutOfRangeException(); + res = value; + }); + + IGenericContainer container = stub; + container[0] = 13; + + Assert.AreEqual(13, res); + } + + [TestMethod] + public void TestThatMultipleIndexerDontConflict() + { + var stub = new StubIGenericContainer(); + stub.Item_Get(index => 12).Item_Get((key, i) => 3); + + IGenericContainer container = stub; + Assert.AreEqual(12, container[0]); + Assert.AreEqual(3, container["foo", 0]); + } + // this test is only used for debugging + [Ignore] [TestMethod] public async Task TestGenerateStubs() {