At times AutoFixture out-of-a-box behavior and capabilities, fall to short when we require more customized test data. Such was my case. In previous post [click] I mentioned that I prefer not to provide objects with invalid data, as a matter of fact, it’s not only a case of personal preference. If we declare array of certain length via MarshalAsAttribute, and then set different one, will end up with exception. Thus the idea of generating test data based on MarshalAsAttribute, since we already have access to it, shouldn’t be a problem. One more thing worth mentioning. At some point I’ll be mocking ADS client, since CI build server won’t have access to actual TwinCAT runtime. Default AutoFixture behavior throws exception if we try to generate structure with default constructor only, if we only go with suggested SupportMutableValueTypesCustomization we’ll end up with structure which has all fields set with default values, isn’t the thing we need, is it?
Implementing ISpecimenBuilder
First thing we ought to do is implementing ISpecimenBuilder and it’s quite simple as it only has one method.
let MarshalAsTypeBuilder = { new ISpecimenBuilder with member self.Create(request, context) = match request with | : ? Type as t when t.IsValueType && t.IsPrimitive |> not && Array.contains t [| typedefof<string>; typedefof<decimal>; typedefof<DateTime>; typedefof<DateTimeOffset>; typedefof<TimeSpan> |] |> not -> let instance = Activator.CreateInstance(t) instance | _ -> new NoSpecimen() :> obj }
What we’re doing here is verifying if the current context is type of our interest. If not we fall back to default behavior on line 20. As you may noticed we still only have an instance with default values.
Providing data for struct fields
before we proceed, let’s take a look on code compiled from F#.
[<StructLayout(LayoutKind.Sequential, Pack=1)>] type StringTestStruct = struct [<MarshalAs(UnmanagedType.ByValTStr, SizeConst=81)>] val string80Var:string [<MarshalAs(UnmanagedType.ByValTStr, SizeConst=2)>] val string1Var:string [<MarshalAs(UnmanagedType.ByValTStr, SizeConst=257)>] val string256Var:string end
[CompilationMapping(SourceConstructFlags.ObjectType)] [Serializable] public struct StringTestStruct : IEquatable<Tests.StringTestStruct>, IStructuralEquatable, IComparable<Tests.StringTestStruct>, IComparable, IStructuralComparable { internal string string80Var@; internal string string1Var@; internal string string256Var@; [CompilationMapping(SourceConstructFlags.Field, 1)] public string string1Var { get { return this.string1Var@; } } [CompilationMapping(SourceConstructFlags.Field, 2)] public string string256Var { get { return this.string256Var@; } } [CompilationMapping(SourceConstructFlags.Field, 0)] public string string80Var { get { return this.string80Var@; } } }
F# hides struct fields behind properties. Properties itself are of no interest for us as they do not store MarshalAsAttribute metadata, thus we’ll be iterating over non-public instance fields.
let instance = Activator.CreateInstance(t) t.GetFields(BindingFlags.Instance ||| BindingFlags.NonPublic ||| BindingFlags.Public) |> Array.map (fun fi -> fi,context.Resolve(fi)) |> Array.iter (fun (fi, value) -> fi.SetValue(instance, value) ) instance
At this point we have are supplied with struct instance with default AutoFixture data, which do not correspond to MarshalAsAttribute data.
Assuring valid data
As said in beginning, we have to provide data for arrays and strings accordingly to information we provided via MarshalAsAttribute.
Thus we’ll have to override default behavior for those as well.
Strings not longer that SizeConst
| : ? FieldInfo as fi when fi.FieldType = typedefof<string> -> let attr = fi.GetMarshalAsAttribute() match attr with | Some(maa) when maa.Value = UnmanagedType.ByValTStr && maa.SizeConst > 0 -> let result = context.Resolve(typedefof<string>).ToString() result.Substring(0,Math.Min(result.Length,maa.SizeConst-1)) :> obj | _ -> new NoSpecimen() :> obj
if field is string, and it has MarshalAsAttribute we use AutoFixture default string generator, only difference is, if it’s longer than expected we take substring of it, so it’s length would correspond to SizeConst
–1 is used only due to ADS string null terminator.
Arrays length equal to SizeConst
| : ? FieldInfo as fi when fi.FieldType.IsArray -> let attr = fi.GetMarshalAsAttribute() match attr with | Some(maa) -> let result = Array.CreateInstance(fi.FieldType.GetElementType(), maa.SizeConst) Array.Copy(Array.init maa.SizeConst (fun _ -> context.Resolve(fi.FieldType.GetElementType())), result,maa.SizeConst) result :> obj | _ -> new NoSpecimen() :> obj
again we override default behavior, this time first we create array with length based on SizeConst, then we fill with values generated with AutoFixture (that includes our overrides, if we have nested types)
Recent Comments