Skip to content

Commit

Permalink
Created VerifyAllColumnsBound check (#1949)
Browse files Browse the repository at this point in the history
  • Loading branch information
304NotModified committed Apr 27, 2020
1 parent 61cf6cf commit d233b2a
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 17 deletions.
10 changes: 10 additions & 0 deletions TechTalk.SpecFlow/Assist/InstanceCreationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using System.Linq;

namespace TechTalk.SpecFlow.Assist
{
public class InstanceCreationOptions
{
public bool VerifyAllColumnsBound { get; set; }
}
}
36 changes: 30 additions & 6 deletions TechTalk.SpecFlow/Assist/TEHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ namespace TechTalk.SpecFlow.Assist
{
internal static class TEHelpers
{
internal static T CreateTheInstanceWithTheDefaultConstructor<T>(Table table)
internal static T CreateTheInstanceWithTheDefaultConstructor<T>(Table table, InstanceCreationOptions creationOptions)
{
var instance = (T)Activator.CreateInstance(typeof(T));
LoadInstanceWithKeyValuePairs(table, instance);
LoadInstanceWithKeyValuePairs(table, instance, creationOptions);
return instance;
}

internal static T CreateTheInstanceWithTheValuesFromTheTable<T>(Table table)
internal static T CreateTheInstanceWithTheValuesFromTheTable<T>(Table table, InstanceCreationOptions creationOptions)
{
var constructor = GetConstructorMatchingToColumnNames<T>(table);
if (constructor == null)
Expand All @@ -27,6 +27,8 @@ internal static T CreateTheInstanceWithTheValuesFromTheTable<T>(Table table)

var constructorParameters = constructor.GetParameters();
var parameterValues = new object[constructorParameters.Length];

var members = new List<string>(constructorParameters.Length);
for (var parameterIndex = 0; parameterIndex < constructorParameters.Length; parameterIndex++)
{
var parameter = constructorParameters[parameterIndex];
Expand All @@ -35,10 +37,15 @@ internal static T CreateTheInstanceWithTheValuesFromTheTable<T>(Table table)
where string.Equals(m.MemberName, parameterName, StringComparison.OrdinalIgnoreCase)
select m).FirstOrDefault();
if (member != null)
{
members.Add(member.MemberName);
parameterValues[parameterIndex] = member.GetValue();
}
else if (parameter.HasDefaultValue)
parameterValues[parameterIndex] = parameter.DefaultValue;
}

VerifyAllColumn(table, creationOptions, members);
return (T)constructor.Invoke(parameterValues);
}

Expand Down Expand Up @@ -87,12 +94,29 @@ internal static string NormalizePropertyNameToMatchAgainstAColumnName(string nam
return name.Replace("_", string.Empty).ToIdentifier();
}

internal static void LoadInstanceWithKeyValuePairs(Table table, object instance)
internal static void LoadInstanceWithKeyValuePairs(Table table, object instance, InstanceCreationOptions creationOptions)
{
var membersThatNeedToBeSet = GetMembersThatNeedToBeSet(table, instance.GetType());
var memberHandlers = membersThatNeedToBeSet.ToList();
var memberNames = memberHandlers.Select(h => h.MemberName);

VerifyAllColumn(table, creationOptions, memberNames);

memberHandlers.ForEach(x => x.Setter(instance, x.GetValue()));
}

membersThatNeedToBeSet.ToList()
.ForEach(x => x.Setter(instance, x.GetValue()));
private static void VerifyAllColumn(Table table, InstanceCreationOptions creationOptions, IEnumerable<string> memberNames)
{
if (creationOptions?.VerifyAllColumnsBound == true)
{
var memberNameKeys = new HashSet<string>(memberNames);
var allIds = table.Rows.Select(r => r.Id()).ToList();
var missing = allIds.Where(m => !memberNameKeys.Contains(m)).ToList();
if (missing.Any())
{
throw new ColumnCouldNotBeBoundException(missing);
}
}
}

internal static IEnumerable<MemberHandler> GetMembersThatNeedToBeSet(Table table, Type type)
Expand Down
44 changes: 37 additions & 7 deletions TechTalk.SpecFlow/Assist/TableExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,56 +7,86 @@ namespace TechTalk.SpecFlow.Assist
public static class TableHelperExtensionMethods
{
public static T CreateInstance<T>(this Table table)
{
return CreateInstance<T>(table, (InstanceCreationOptions)null);
}

public static T CreateInstance<T>(this Table table, InstanceCreationOptions creationOptions)
{
var instanceTable = TEHelpers.GetTheProperInstanceTable(table, typeof(T));
return TEHelpers.ThisTypeHasADefaultConstructor<T>()
? TEHelpers.CreateTheInstanceWithTheDefaultConstructor<T>(instanceTable)
: TEHelpers.CreateTheInstanceWithTheValuesFromTheTable<T>(instanceTable);
? TEHelpers.CreateTheInstanceWithTheDefaultConstructor<T>(instanceTable, creationOptions)
: TEHelpers.CreateTheInstanceWithTheValuesFromTheTable<T>(instanceTable, creationOptions);
}

public static T CreateInstance<T>(this Table table, Func<T> methodToCreateTheInstance)
{
return CreateInstance(table, methodToCreateTheInstance, null);
}

public static T CreateInstance<T>(this Table table, Func<T> methodToCreateTheInstance, InstanceCreationOptions creationOptions)
{
var instance = methodToCreateTheInstance();
table.FillInstance(instance);
table.FillInstance(instance, creationOptions);
return instance;
}

public static void FillInstance(this Table table, object instance)
{
FillInstance(table, instance, null);
}

public static void FillInstance(this Table table, object instance, InstanceCreationOptions creationOptions)
{
var instanceTable = TEHelpers.GetTheProperInstanceTable(table, instance.GetType());
TEHelpers.LoadInstanceWithKeyValuePairs(instanceTable, instance);
TEHelpers.LoadInstanceWithKeyValuePairs(instanceTable, instance, creationOptions);
}

public static IEnumerable<T> CreateSet<T>(this Table table)
{
return CreateSet<T>(table, (InstanceCreationOptions)null);
}

public static IEnumerable<T> CreateSet<T>(this Table table, InstanceCreationOptions creationOptions)
{
var list = new List<T>();

var pivotTable = new PivotTable(table);
for (var index = 0; index < table.Rows.Count(); index++)
{
var instance = pivotTable.GetInstanceTable(index).CreateInstance<T>();
var instance = pivotTable.GetInstanceTable(index).CreateInstance<T>(creationOptions);
list.Add(instance);
}

return list;
}

public static IEnumerable<T> CreateSet<T>(this Table table, Func<T> methodToCreateEachInstance)
{
return CreateSet(table, methodToCreateEachInstance, null);
}

public static IEnumerable<T> CreateSet<T>(this Table table, Func<T> methodToCreateEachInstance, InstanceCreationOptions creationOptions)
{
var list = new List<T>();

var pivotTable = new PivotTable(table);
for (var index = 0; index < table.Rows.Count(); index++)
{
var instance = methodToCreateEachInstance();
pivotTable.GetInstanceTable(index).FillInstance(instance);
pivotTable.GetInstanceTable(index).FillInstance(instance, creationOptions);
list.Add(instance);
}

return list;
}

public static IEnumerable<T> CreateSet<T>(this Table table, Func<TableRow, T> methodToCreateEachInstance)
{
return CreateSet(table, methodToCreateEachInstance, null);
}

public static IEnumerable<T> CreateSet<T>(this Table table, Func<TableRow, T> methodToCreateEachInstance, InstanceCreationOptions creationOptions)
{
var list = new List<T>();

Expand All @@ -65,7 +95,7 @@ public static IEnumerable<T> CreateSet<T>(this Table table, Func<TableRow, T> me
{
var row = table.Rows[index];
var instance = methodToCreateEachInstance(row);
pivotTable.GetInstanceTable(index).FillInstance(instance);
pivotTable.GetInstanceTable(index).FillInstance(instance, creationOptions);
list.Add(instance);
}

Expand Down
55 changes: 55 additions & 0 deletions TechTalk.SpecFlow/ErrorHandling/ColumnCouldNotBeBoundException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;


// the exceptions are part of the public API, keep them in TechTalk.SpecFlow namespace
namespace TechTalk.SpecFlow
{
[Serializable]
public class ColumnCouldNotBeBoundException : Exception
{
public IList<string> Columns { get; }

/// <inheritdoc />
public ColumnCouldNotBeBoundException()
{
}

/// <inheritdoc />
public ColumnCouldNotBeBoundException(IList<string> columns) : base(CreateMessage(columns))
{
Columns = columns;
}

/// <inheritdoc />
public ColumnCouldNotBeBoundException(string message, IList<string> columns) : base(message)
{
Columns = columns;
}

/// <inheritdoc />
public ColumnCouldNotBeBoundException(string message, Exception innerException, IList<string> columns) : base(message, innerException)
{
Columns = columns;
}

/// <inheritdoc />
protected ColumnCouldNotBeBoundException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}

private static string CreateMessage(IList<string> columns)
{
if (columns.Count == 1)
{
return $"Member or field {string.Join(",", columns)} not found";
}
return $"Members or fields {string.Join(",", columns)} not found";

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace TechTalk.SpecFlow.RuntimeTests.AssistTests
{

public class CreateInstanceHelperMethodTests
{
public CreateInstanceHelperMethodTests()
Expand Down Expand Up @@ -38,6 +38,16 @@ public void Create_instance_will_set_values_with_a_vertical_table_when_there_is_
person.FirstName.Should().Be("Howard");
}

[Fact]
public void Create_instance_will_set_values_with_a_vertical_table_and_unbound_column_throws_ColumnCouldNotBeBoundException_on_verify()
{
var table = new Table("FirstNaame");
table.AddRow("Howard");

Action act = () => table.CreateInstance<Person>(new InstanceCreationOptions() { VerifyAllColumnsBound = true });
act.Should().Throw<ColumnCouldNotBeBoundException>();
}

[Fact]
public void When_one_row_exists_with_two_headers_and_the_first_row_value_is_not_a_property_then_treat_as_horizontal_table()
{
Expand Down Expand Up @@ -104,6 +114,28 @@ public void Sets_string_values()
person.LastName.Should().Be("Galt");
}

[Fact]
public void Sets_string_values_unbound_column_throws_ColumnCouldNotBeBoundException_on_verify()
{
var table = new Table("Field", "Value");
table.AddRow("FirstNaame", "John");
table.AddRow("LastName", "Galt");

Action act = () => table.CreateInstance<Person>(new InstanceCreationOptions { VerifyAllColumnsBound = true });
act.Should().Throw<ColumnCouldNotBeBoundException>();
}

[Fact]
public void SetConstructor_unbound_column_throws_ColumnCouldNotBeBoundException_on_verify()
{
var table = new Table("Field", "Value");
table.AddRow("FirstNaame", "John");
table.AddRow("LastName", "Galt");

Action act = () => table.CreateInstance<PersonWithMandatoryLastName>(new InstanceCreationOptions { VerifyAllColumnsBound = true });
act.Should().Throw<ColumnCouldNotBeBoundException>();
}

[Fact]
public void Sets_int_values()
{
Expand Down Expand Up @@ -352,7 +384,7 @@ public void Too_long_tuples_throw_exception()
var table = new Table("PropertyOne", "PropertyTwo", "PropertyThree", "PropertyFour", "PropertyFive", "PropertySix", "PropertySeven", "PropertyEight");
table.AddRow("Look at me", "hello", "999", "this", "should", "actually", "fail", "right?");

Assert.Throws<Exception>(() => table.CreateInstance<(string one, string two, int three, string four, string five, string six, string seven, string eight)>());
Assert.Throws<Exception>(() => table.CreateInstance<(string one, string two, int three, string four, string five, string six, string seven, string eight)>());
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace TechTalk.SpecFlow.RuntimeTests.AssistTests.ExampleEntities
{
class PersonWithMandatoryLastName : Person
{
/// <inheritdoc />
public PersonWithMandatoryLastName(string lastName)
{
LastName = lastName ?? throw new ArgumentNullException(nameof(lastName));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace TechTalk.SpecFlow.RuntimeTests.AssistTests.TableHelperExtensionMethods
{

public class CreateSetHelperMethodTests
{
public CreateSetHelperMethodTests()
Expand Down Expand Up @@ -95,6 +95,17 @@ public void Returns_two_instances_when_there_are_two_rows()
people.Count().Should().Be(2);
}

[Fact]
public void two_instances_with_unbound_column_throws_ColumnCouldNotBeBoundException_on_verify()
{
var table = new Table("SurName");
table.AddRow("John");
table.AddRow("Howard");
Action act = () => table.CreateSet<Person>(new InstanceCreationOptions { VerifyAllColumnsBound = true });

act.Should().Throw<ColumnCouldNotBeBoundException>();
}

[Fact]
public void Sets_string_values_on_the_instance_when_type_is_string()
{
Expand Down Expand Up @@ -142,7 +153,7 @@ public void Sets_datetime_on_the_instance_when_type_is_datetime()
people.First().BirthDate.Should().Be(new DateTime(2009, 4, 28));
}


[Fact]
public void Sets_datetime_on_the_instance_when_type_is_datetime_and_culture_is_fr_FR()
{
Expand Down
3 changes: 3 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Fixes:
+ Save generated files with UTF8-BOM encoding.
+ Revert "Replace NUnit.Framework.DescriptionAttribute to TestName in NUnit generator #1225" because of problems with the NUnit Test Adapter

Features:
+ Created VerifyAllColumnsBound check for `table.CreateSet` and `table.CreateInstance`

Changes since 3.1.86

Fixes:
Expand Down

0 comments on commit d233b2a

Please sign in to comment.