Add a couple of scopes useful when unit testing display and editor helpers

- `MetadataOverrideScope` and `TemplateHelpersSafeScope`
This commit is contained in:
dougbu 2014-06-23 11:36:59 -07:00 коммит произвёл Doug Bunting
Родитель db6c437ae2
Коммит 20de11cb96
3 изменённых файлов: 224 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,168 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using Moq;
namespace System.Web.Mvc.Html.Test
{
/// <summary>
/// <para>
/// A scope within which the <see cref="ModelMetadata"/> for a single <see cref="Type"/> is overridden. Could be
/// used for example to ensure the metadata for <see cref="Exception"/> includes an additional property or
/// has a non-<c>null</c> <see cref="ModelMetadata.DisplayName"/>.
/// </para>
/// <para>
/// Notes: Does _not_ override the metadata for subclasses of the given <see cref="Type"/>. And callers should
/// override (likely, mock) the metadata of the containing <see cref="Type"/> when changing the metadata of a
/// property e.g. modifying <see cref="ModelMetadata.IsRequired"/>.
/// </para>
/// </summary>
public class MetadataOverrideScope : IDisposable
{
private static readonly DataAnnotationsModelMetadataProvider AnnotationsProvider =
new DataAnnotationsModelMetadataProvider();
private readonly ModelMetadataProvider _oldMetadataProvider;
private readonly ModelMetadata _metadata;
private readonly Type _modelType;
public MetadataOverrideScope(ModelMetadata metadata)
{
if (metadata == null)
{
throw new ArgumentNullException("metadata");
}
if (metadata.ModelType == null)
{
throw new ArgumentException("Need ModelType", "metadata");
}
_oldMetadataProvider = ModelMetadataProviders.Current;
_metadata = metadata;
_modelType = metadata.ModelType;
// Mock a ModelMetadataProvider which delegates to the old one in most cases. No need to special-case
// GetMetadataForProperties() because product code uses it only within ModelMetadata.Properties and our
// metadata instance will call _oldMetadataProvider there.
var metadataProvider = new Mock<ModelMetadataProvider>();
metadataProvider
.Setup(p => p.GetMetadataForProperties(It.IsAny<object>(), It.IsAny<Type>()))
.Returns((object container, Type containerType) =>
_oldMetadataProvider.GetMetadataForProperties(container, containerType));
metadataProvider
.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
.Returns((Func<object> modelAccessor, Type modelType) =>
_oldMetadataProvider.GetMetadataForType(modelAccessor, modelType));
// When metadata for _modelType is requested, then return a clone of the provided metadata instance.
// GetMetadataForProperty() is important because the static discovery methods (e.g.
// ModelMetadata.FromLambdaExpression) use it.
metadataProvider
.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), _modelType))
.Returns((Func<object> modelAccessor, Type modelType) => GetMetadataForType(modelAccessor, modelType));
metadataProvider
.Setup(p =>
p.GetMetadataForProperty(It.IsAny<Func<object>>(), It.IsAny<Type>(), It.IsAny<string>()))
.Returns((Func<object> modelAccessor, Type containerType, string propertyName) =>
GetMetadataForProperty(modelAccessor, containerType, propertyName));
// Calls to GetMetadataForProperties for the modelType are incorrect because _metadata.Provider must
// reference _oldMetadataProvider and not this mock.
metadataProvider
.Setup(p => p.GetMetadataForProperty(It.IsAny<Func<object>>(), _modelType, It.IsAny<string>()))
.Throws<InvalidOperationException>();
// Finally make our ModelMetadataProvider visible everywhere.
ModelMetadataProviders.Current = metadataProvider.Object;
}
public void Dispose()
{
ModelMetadataProviders.Current = _oldMetadataProvider;
}
private ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType)
{
return CloneMetadata(modelAccessor);
}
private ModelMetadata GetMetadataForProperty(
Func<object> modelAccessor,
Type containerType,
string propertyName)
{
var propertyMetadata =
_oldMetadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName);
if (propertyMetadata == null)
{
return null;
}
if (propertyMetadata.ModelType == _modelType)
{
return CloneMetadata(() => propertyMetadata.Model);
}
return propertyMetadata;
}
private ModelMetadata CloneMetadata(Func<object> modelAccessor)
{
var cachedMetadata = _metadata as CachedDataAnnotationsModelMetadata;
var annotationsMetadata = _metadata as DataAnnotationsModelMetadata;
ModelMetadata clonedMetadata;
if (cachedMetadata != null)
{
clonedMetadata = new CachedDataAnnotationsModelMetadata(cachedMetadata, modelAccessor);
}
else if (annotationsMetadata != null)
{
var provider = (_oldMetadataProvider as DataAnnotationsModelMetadataProvider) ?? AnnotationsProvider;
clonedMetadata = new DataAnnotationsModelMetadata(
provider,
annotationsMetadata.ContainerType,
modelAccessor,
_modelType,
annotationsMetadata.PropertyName,
displayColumnAttribute: null); // Copying SimpleDisplayText below compensates for null here.
}
else
{
clonedMetadata = new ModelMetadata(
_oldMetadataProvider,
_metadata.ContainerType,
modelAccessor,
_modelType,
_metadata.PropertyName);
}
// Undo all the lazy-initialization of ModelMetadata and CachedDataAnnotationsModelMetadata...
clonedMetadata.Container = _metadata.Container; // May be incorrect.
clonedMetadata.ConvertEmptyStringToNull = _metadata.ConvertEmptyStringToNull;
clonedMetadata.DataTypeName = _metadata.DataTypeName;
clonedMetadata.Description = _metadata.Description;
clonedMetadata.DisplayFormatString = _metadata.DisplayFormatString;
clonedMetadata.DisplayName = _metadata.DisplayName;
clonedMetadata.EditFormatString = _metadata.EditFormatString;
clonedMetadata.HasNonDefaultEditFormat = _metadata.HasNonDefaultEditFormat;
clonedMetadata.HideSurroundingHtml = _metadata.HideSurroundingHtml;
clonedMetadata.HtmlEncode = _metadata.HtmlEncode;
clonedMetadata.IsReadOnly = _metadata.IsReadOnly;
clonedMetadata.IsRequired = _metadata.IsRequired;
clonedMetadata.NullDisplayText = _metadata.NullDisplayText;
clonedMetadata.Order = _metadata.Order;
clonedMetadata.RequestValidationEnabled = _metadata.RequestValidationEnabled;
clonedMetadata.ShortDisplayName = _metadata.ShortDisplayName;
clonedMetadata.ShowForDisplay = _metadata.ShowForDisplay;
clonedMetadata.ShowForEdit = _metadata.ShowForEdit;
clonedMetadata.SimpleDisplayText = _metadata.SimpleDisplayText;
clonedMetadata.TemplateHint = _metadata.TemplateHint;
clonedMetadata.Watermark = _metadata.Watermark;
foreach (var keyValuePair in _metadata.AdditionalValues)
{
clonedMetadata.AdditionalValues.Add(keyValuePair.Key, keyValuePair.Value);
}
return clonedMetadata;
}
}
}

Просмотреть файл

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using System.Web.WebPages.Scope;
using Moq;
namespace System.Web.Mvc.Html.Test
{
/// <summary>
/// A scope within which it is safe to invoke <see cref="TemplateHelpers"/> methods. For example
/// <see cref="TemplateHelpers.ExecuteTemplate()"/> invokes <code>ViewEngines.Engines.FindPartialView()</code> and
/// clones the current <see cref="ViewContext"/>.
/// </summary>
/// <remarks>Similar to TemplateHelpersTest.MockViewEngine but FindPartialView() succeed there and fail here. In
/// addition TemplateHelpersTest tests do not continue far enough to need the transient scope.
/// </remarks>
public class TemplateHelpersSafeScope : IDisposable
{
private readonly List<IViewEngine> _oldEngines;
private IDisposable _transientScope;
public TemplateHelpersSafeScope()
{
// Copying an HtmlHelper instance reads and writes the current StorageScope.
// Ensure that's not the global scope.
_transientScope = ScopeStorage.CreateTransientScope();
// Do not want templates to check disk for anything.
var engine = new Mock<IViewEngine>(MockBehavior.Strict);
engine
.Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), It.IsAny<string>(), It.IsAny<bool>()))
.Returns(new ViewEngineResult(Enumerable.Empty<string>()));
_oldEngines = ViewEngines.Engines.ToList();
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(engine.Object);
}
public void Dispose()
{
ViewEngines.Engines.Clear();
foreach (var oldEngine in _oldEngines)
{
ViewEngines.Engines.Add(oldEngine);
}
using (_transientScope)
{
_transientScope = null;
}
}
}
}

Просмотреть файл

@ -56,7 +56,9 @@
<Compile Include="Html\Test\DisplayTextExtensionsTest.cs" /> <Compile Include="Html\Test\DisplayTextExtensionsTest.cs" />
<Compile Include="Html\Test\EncodedDataSets.cs" /> <Compile Include="Html\Test\EncodedDataSets.cs" />
<Compile Include="Html\Test\EnumHelperTest.cs" /> <Compile Include="Html\Test\EnumHelperTest.cs" />
<Compile Include="Html\Test\MetadataOverrideScope.cs" />
<Compile Include="Html\Test\NameExtensionsTest.cs" /> <Compile Include="Html\Test\NameExtensionsTest.cs" />
<Compile Include="Html\Test\TemplateHelpersSafeScope.cs" />
<Compile Include="Html\Test\ValueExtensionsTest.cs" /> <Compile Include="Html\Test\ValueExtensionsTest.cs" />
<Compile Include="Test\CopyOnWriteDictionaryTest.cs" /> <Compile Include="Test\CopyOnWriteDictionaryTest.cs" />
<Compile Include="Test\JQueryFormValueProviderFactoryTest.cs" /> <Compile Include="Test\JQueryFormValueProviderFactoryTest.cs" />