Skip to content

Commit 6c78359

Browse files
committed
Implemented ViewComponentSetupBuilder (#66)
1 parent 0dc9014 commit 6c78359

23 files changed

Lines changed: 419 additions & 101 deletions

File tree

samples/MusicStore/MusicStore.Test/TestStartup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public void ConfigureTestServices(IServiceCollection services)
4040
TestHelper.ShouldPassForPlugins.Add(new ControllersTestPlugin());
4141

4242
services
43-
.AddMvcTesting()
43+
.AddMvcUniverseTesting()
4444
.AddRoutingTesting();
4545
#endif
4646
}

src/MyTested.AspNetCore.Mvc.Abstractions/Builders/Components/BaseComponentBuilder.cs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public abstract class BaseComponentBuilder<TComponent, TTestContext, TBuilder> :
2020
private TTestContext testContext;
2121
private bool isPreparedForTesting;
2222

23+
private Action<TComponent> componentSetupAction;
24+
2325
public BaseComponentBuilder(TTestContext testContext)
2426
: base(testContext)
2527
{
@@ -63,7 +65,15 @@ protected TComponent Component
6365
protected abstract string ComponentName { get; }
6466

6567
protected abstract bool IsValidComponent { get; }
68+
69+
protected bool SkipComponentActivation { get; set; }
6670

71+
public TBuilder WithSetup(Action<TComponent> componentSetup)
72+
{
73+
this.componentSetupAction += componentSetup;
74+
return this.Builder;
75+
}
76+
6777
protected virtual void BuildComponentIfNotExists()
6878
{
6979
if (!this.isPreparedForTesting)
@@ -82,8 +92,18 @@ protected virtual void BuildComponentIfNotExists()
8292
}
8393
else
8494
{
85-
// no custom dependencies are set, try create instance with the global services
86-
component = TestHelper.TryCreateInstance<TComponent>();
95+
// no custom dependencies are set, try create instance with component factory
96+
component = this.TryCreateComponentWithFactory();
97+
98+
if (component != null)
99+
{
100+
this.SkipComponentActivation = true;
101+
}
102+
else
103+
{
104+
// no component from the factory, try create instance with the global services
105+
component = TestHelper.TryCreateInstance<TComponent>();
106+
}
87107
}
88108

89109
if (component == null && !explicitDependenciesAreSet)
@@ -126,6 +146,20 @@ protected void ValidateComponentType()
126146

127147
protected abstract void PrepareComponentContext();
128148

129-
protected abstract void PrepareComponent();
149+
protected abstract TComponent TryCreateComponentWithFactory();
150+
151+
protected abstract void ActivateComponent();
152+
153+
private void PrepareComponent()
154+
{
155+
if (!this.SkipComponentActivation)
156+
{
157+
this.ActivateComponent();
158+
}
159+
160+
this.TestContext.ComponentPreparationDelegate?.Invoke();
161+
162+
this.componentSetupAction?.Invoke(this.TestContext.ComponentAs<TComponent>());
163+
}
130164
}
131165
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
namespace MyTested.AspNetCore.Mvc.Internal
2+
{
3+
using System;
4+
using System.Linq;
5+
using System.Reflection;
6+
using Utilities.Validators;
7+
8+
public class PropertyActivator<TContext>
9+
{
10+
private readonly Func<TContext, object> valueAccessor;
11+
private readonly Action<object, object> fastPropertySetter;
12+
13+
public PropertyActivator(
14+
PropertyInfo propertyInfo,
15+
Func<TContext, object> valueAccessor)
16+
{
17+
CommonValidator.CheckForNullReference(propertyInfo, nameof(propertyInfo));
18+
CommonValidator.CheckForNullReference(valueAccessor, nameof(valueAccessor));
19+
20+
this.PropertyInfo = propertyInfo;
21+
this.valueAccessor = valueAccessor;
22+
this.fastPropertySetter = PropertyHelper.MakeFastPropertySetter(propertyInfo);
23+
}
24+
25+
public PropertyInfo PropertyInfo { get; private set; }
26+
27+
public object Activate(object instance, TContext context)
28+
{
29+
if (instance == null)
30+
{
31+
throw new ArgumentNullException(nameof(instance));
32+
}
33+
34+
var value = valueAccessor(context);
35+
fastPropertySetter(instance, value);
36+
return value;
37+
}
38+
39+
public static PropertyActivator<TContext>[] GetPropertiesToActivate(
40+
Type type,
41+
Type activateAttributeType,
42+
Func<PropertyInfo, PropertyActivator<TContext>> createActivateInfo)
43+
{
44+
CommonValidator.CheckForNullReference(type, nameof(type));
45+
CommonValidator.CheckForNullReference(activateAttributeType, nameof(activateAttributeType));
46+
CommonValidator.CheckForNullReference(createActivateInfo, nameof(createActivateInfo));
47+
48+
return type.GetRuntimeProperties()
49+
.Where(property => property.IsDefined(activateAttributeType)
50+
&& property.GetIndexParameters().Length == 0
51+
&& property.SetMethod != null
52+
&& property.SetMethod.IsPublic
53+
&& !property.SetMethod.IsStatic)
54+
.Select(createActivateInfo)
55+
.ToArray();
56+
}
57+
}
58+
}

src/MyTested.AspNetCore.Mvc.Abstractions/Internal/BasePropertyHelper.cs renamed to src/MyTested.AspNetCore.Mvc.Abstractions/Internal/PropertyHelper.cs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
using System.Reflection;
77
using Utilities;
88

9-
public abstract class BasePropertyHelper
9+
public abstract class PropertyHelper
1010
{
1111
private const string InvalidDelegateErrorMessage = "The {0} property cannot be activated for value of {1} type.";
1212

1313
private static readonly MethodInfo CallPropertyGetterOpenGenericMethod =
14-
typeof(BasePropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyGetter));
14+
typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyGetter));
1515

16-
protected BasePropertyHelper(Type type)
16+
private static readonly MethodInfo CallPropertySetterOpenGenericMethod =
17+
typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertySetter));
18+
19+
protected PropertyHelper(Type type)
1720
{
1821
this.Type = type;
1922
this.Properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
@@ -23,7 +26,7 @@ protected BasePropertyHelper(Type type)
2326

2427
protected IEnumerable<PropertyInfo> Properties { get; private set; }
2528

26-
protected static Func<object, TResult> MakeFastPropertyGetter<TResult>(PropertyInfo propertyInfo)
29+
public static Func<object, TResult> MakeFastPropertyGetter<TResult>(PropertyInfo propertyInfo)
2730
{
2831
try
2932
{
@@ -48,6 +51,25 @@ protected static Func<object, TResult> MakeFastPropertyGetter<TResult>(PropertyI
4851
}
4952
}
5053

54+
public static Action<object, object> MakeFastPropertySetter(PropertyInfo propertyInfo)
55+
{
56+
var setMethod = propertyInfo.SetMethod;
57+
var parameters = setMethod.GetParameters();
58+
59+
var typeInput = setMethod.DeclaringType;
60+
var parameterType = parameters[0].ParameterType;
61+
62+
var propertySetterAsAction =
63+
setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(typeInput, parameterType));
64+
var callPropertySetterClosedGenericMethod =
65+
CallPropertySetterOpenGenericMethod.MakeGenericMethod(typeInput, parameterType);
66+
var callPropertySetterDelegate =
67+
callPropertySetterClosedGenericMethod.CreateDelegate(
68+
typeof(Action<object, object>), propertySetterAsAction);
69+
70+
return (Action<object, object>)callPropertySetterDelegate;
71+
}
72+
5173
protected PropertyInfo FindPropertyWithAttribute<TAttribute>()
5274
where TAttribute : Attribute
5375
{
@@ -69,5 +91,14 @@ private static TValue CallPropertyGetter<TDeclaringType, TValue>(
6991
{
7092
return getter((TDeclaringType)target);
7193
}
94+
95+
// Called via reflection
96+
private static void CallPropertySetter<TDeclaringType, TValue>(
97+
Action<TDeclaringType, TValue> setter,
98+
object target,
99+
object value)
100+
{
101+
setter((TDeclaringType)target, (TValue)value);
102+
}
72103
}
73104
}

src/MyTested.AspNetCore.Mvc.Abstractions/Internal/TestContexts/ActionTestContext.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace MyTested.AspNetCore.Mvc.Internal.TestContexts
22
{
3+
using System;
34
using System.Linq;
45
using Application;
56
using Microsoft.AspNetCore.Mvc;
@@ -62,6 +63,16 @@ public TComponentContext ComponentContext
6263
}
6364
}
6465

66+
public Action<TComponentContext> ComponentContextPreparationDelegate { get; set; }
67+
6568
protected abstract TComponentContext DefaultComponentContext { get; }
69+
70+
public void Apply<TMethodResult>(InvocationTestContext<TMethodResult> invocationTestContext)
71+
{
72+
this.MethodName = invocationTestContext.MethodName;
73+
this.MethodCall = invocationTestContext.MethodCall;
74+
this.MethodResult = invocationTestContext.MethodResult;
75+
this.CaughtException = invocationTestContext.CaughtException;
76+
}
6677
}
6778
}

src/MyTested.AspNetCore.Mvc.Controllers/Builders/Controllers/ControllerBuilder.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
namespace MyTested.AspNetCore.Mvc.Builders.Controllers
22
{
3-
using System;
43
using Components;
54
using Contracts.Controllers;
65
using Internal.Application;
76
using Internal.Contracts;
87
using Internal.TestContexts;
9-
using Microsoft.AspNetCore.Mvc;
108
using Microsoft.Extensions.DependencyInjection;
119

1210
/// <summary>
@@ -16,9 +14,6 @@
1614
public partial class ControllerBuilder<TController> : BaseComponentBuilder<TController, ControllerTestContext, IAndControllerBuilder<TController>>, IAndControllerBuilder<TController>
1715
where TController : class
1816
{
19-
private Action<ControllerContext> controllerContextAction;
20-
private Action<TController> controllerSetupAction;
21-
2217
/// <summary>
2318
/// Initializes a new instance of the <see cref="ControllerBuilder{TController}"/> class.
2419
/// </summary>
Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
namespace MyTested.AspNetCore.Mvc.Builders.Controllers
22
{
33
using System;
4+
using System.Reflection;
45
using Contracts.Controllers;
56
using Internal.Controllers;
67
using Microsoft.AspNetCore.Mvc;
7-
using Microsoft.AspNetCore.Mvc.Internal;
88
using Microsoft.Extensions.DependencyInjection;
99
using Utilities.Extensions;
1010
using Utilities.Validators;
11+
using Microsoft.AspNetCore.Mvc.Controllers;
12+
using Microsoft.AspNetCore.Mvc.Internal;
1113

1214
/// <content>
1315
/// Used for building the controller which will be tested.
@@ -23,7 +25,7 @@ public IAndControllerBuilder<TController> WithControllerContext(ControllerContex
2325
/// <inheritdoc />
2426
public IAndControllerBuilder<TController> WithControllerContext(Action<ControllerContext> controllerContextSetup)
2527
{
26-
this.controllerContextAction += controllerContextSetup;
28+
this.TestContext.ComponentContextPreparationDelegate += controllerContextSetup;
2729
return this;
2830
}
2931

@@ -38,32 +40,36 @@ public IAndControllerBuilder<TController> WithActionContext(ActionContext action
3840
/// <inheritdoc />
3941
public IAndControllerBuilder<TController> WithActionContext(Action<ActionContext> actionContextSetup)
4042
{
41-
this.controllerContextAction += actionContextSetup;
42-
return this;
43-
}
44-
45-
/// <inheritdoc />
46-
public IAndControllerBuilder<TController> WithSetup(Action<TController> controllerSetup)
47-
{
48-
this.controllerSetupAction += controllerSetup;
43+
this.TestContext.ComponentContextPreparationDelegate += actionContextSetup;
4944
return this;
5045
}
5146

5247
protected override void PrepareComponentContext()
5348
{
5449
var controllerContext = this.TestContext.ComponentContext;
55-
this.controllerContextAction?.Invoke(controllerContext);
50+
controllerContext.ActionDescriptor.ControllerTypeInfo = typeof(TController).GetTypeInfo();
51+
this.TestContext.ComponentContextPreparationDelegate?.Invoke(controllerContext);
52+
}
53+
54+
protected override TController TryCreateComponentWithFactory()
55+
{
56+
try
57+
{
58+
return this.Services
59+
.GetService<IControllerFactory>()
60+
?.CreateController(this.TestContext.ComponentContext) as TController;
61+
}
62+
catch
63+
{
64+
return null;
65+
}
5666
}
5767

58-
protected override void PrepareComponent()
68+
protected override void ActivateComponent()
5969
{
6070
this.Services
6171
.GetServices<IControllerPropertyActivator>()
6272
?.ForEach(a => a.Activate(this.TestContext.ComponentContext, this.TestContext.Component));
63-
64-
this.TestContext.ComponentPreparationDelegate?.Invoke();
65-
66-
this.controllerSetupAction?.Invoke(this.TestContext.ComponentAs<TController>());
6773
}
6874
}
6975
}

src/MyTested.AspNetCore.Mvc.Controllers/Internal/Controllers/ControllerContextMock.cs

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,48 +9,31 @@
99

1010
public class ControllerContextMock : ControllerContext
1111
{
12-
private HttpTestContext testContext;
13-
14-
private ControllerContextMock(HttpTestContext testContext, ActionContext actionContext)
12+
private ControllerContextMock(ActionContext actionContext)
1513
: base(actionContext)
1614
{
17-
this.PrepareControllerContext(testContext);
15+
this.PrepareControllerContext(actionContext);
1816
}
19-
20-
private HttpTestContext TestContext
21-
{
22-
get
23-
{
24-
return this.testContext;
25-
}
26-
27-
set
28-
{
29-
CommonValidator.CheckForNullReference(value, nameof(TestContext));
30-
this.testContext = value;
31-
}
32-
}
33-
17+
3418
public static ControllerContext Default(HttpTestContext testContext)
3519
=> FromActionContext(testContext, new ActionContext());
3620

3721
public static ControllerContext FromActionContext(HttpTestContext testContext, ActionContext actionContext)
3822
{
3923
CommonValidator.CheckForNullReference(testContext, nameof(HttpTestContext));
4024
CommonValidator.CheckForNullReference(actionContext, nameof(ActionContext));
41-
25+
4226
actionContext.HttpContext = actionContext.HttpContext ?? testContext.HttpContext;
4327
actionContext.RouteData = actionContext.RouteData ?? testContext.RouteData ?? new RouteData();
4428
actionContext.ActionDescriptor = actionContext.ActionDescriptor ?? ActionDescriptorMock.Default;
4529

46-
return new ControllerContextMock(testContext, actionContext);
30+
return new ControllerContextMock(actionContext);
4731
}
4832

49-
private void PrepareControllerContext(HttpTestContext testContext)
33+
private void PrepareControllerContext(ActionContext actionContext)
5034
{
51-
this.TestContext = testContext;
52-
this.HttpContext = testContext.HttpContext;
53-
this.RouteData = testContext.RouteData ?? new RouteData();
35+
this.HttpContext = actionContext.HttpContext;
36+
this.RouteData = actionContext.RouteData;
5437
this.ValueProviderFactories = this.ValueProviderFactories ?? new List<IValueProviderFactory>();
5538

5639
ControllerTestHelper.SetActionContextToAccessor(this);

src/MyTested.AspNetCore.Mvc.Controllers/Internal/Controllers/ControllerPropertyHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using System.Collections.Concurrent;
55
using Microsoft.AspNetCore.Mvc;
66

7-
public class ControllerPropertyHelper : BasePropertyHelper
7+
public class ControllerPropertyHelper : PropertyHelper
88
{
99
private static readonly ConcurrentDictionary<Type, ControllerPropertyHelper> ControllerPropertiesCache =
1010
new ConcurrentDictionary<Type, ControllerPropertyHelper>();

0 commit comments

Comments
 (0)