unit-testing - studio - unit testing vs 2017
Compare la igualdad entre dos objetos en NUnit (18)
Anule los .Equals para su objeto y en la prueba unitaria, puede simplemente hacer esto:
Assert.AreEqual(LeftObject, RightObject);
Por supuesto, esto podría significar que solo mueve todas las comparaciones individuales al método .Equals, pero le permitiría reutilizar esa implementación para múltiples pruebas, y probablemente tenga sentido si los objetos pudieran compararse con hermanos de todos modos.
Intento afirmar que un objeto es "igual" a otro objeto.
Los objetos son solo instancias de una clase con muchas propiedades públicas. ¿Hay alguna manera fácil de hacer que NUnit afirme la igualdad en función de las propiedades?
Esta es mi solución actual, pero creo que puede haber algo mejor:
Assert.AreEqual(LeftObject.Property1, RightObject.Property1)
Assert.AreEqual(LeftObject.Property2, RightObject.Property2)
Assert.AreEqual(LeftObject.Property3, RightObject.Property3)
...
Assert.AreEqual(LeftObject.PropertyN, RightObject.PropertyN)
Lo que estoy buscando sería con el mismo espíritu que el CollectionEquivalentConstraint en el que NUnit verifica que los contenidos de dos colecciones son idénticos.
Deserialice ambas clases y haga una comparación de cuerdas.
EDITAR: Funciona perfectamente, esta es la salida que recibo de NUnit;
Test ''Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.TranslateNew_GivenEaiCustomer_ShouldTranslateToDomainCustomer_Test("ApprovedRatingInDb")'' failed:
Expected string length 2841 but was 5034. Strings differ at index 443.
Expected: "...taClasses" />/r/n <ContactMedia />/r/n <Party i:nil="true" /..."
But was: "...taClasses" />/r/n <ContactMedia>/r/n <ContactMedium z:Id="..."
----------------------------------------------^
TranslateEaiCustomerToDomain_Tests.cs(201,0): at Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.Assert_CustomersAreEqual(Customer expectedCustomer, Customer actualCustomer)
TranslateEaiCustomerToDomain_Tests.cs(114,0): at Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.TranslateNew_GivenEaiCustomer_ShouldTranslateToDomainCustomer_Test(String custRatingScenario)
EDITAR DOS: los dos objetos pueden ser idénticos, pero el orden en el que se serializan las propiedades no es el mismo. Por lo tanto, el XML es diferente. DOH!
EDITAR TRES: Esto funciona. Lo estoy usando en mis pruebas. Pero debe agregar elementos a las propiedades de la colección en el orden en que el código bajo prueba los agrega.
Eche un vistazo al siguiente enlace. Es una solución del proyecto de código y yo también lo he usado. Funciona bien para comparar los objetos.
http://www.codeproject.com/Articles/22709/Testing-Equality-of-Two-Objects?msg=5189539#xx5189539xx
Este es un hilo muy viejo, pero me preguntaba si hay una razón por la cual no se propuso ninguna respuesta NUnit.Framework.Is.EqualTo
y NUnit.Framework.Is.NotEqualTo
?
Como:
Assert.That(LeftObject, Is.EqualTo(RightObject));
y
Assert.That(LeftObject, Is.Not.EqualTo(RightObject));
Estoy de acuerdo con ChrisYoxall: implementar Equals en tu código principal solo con fines de prueba no es bueno.
Si está implementando Equals porque la lógica de la aplicación lo requiere, está bien, pero mantenga limpio el código puro de prueba para que no se complique (también la semántica de verificar lo mismo para las pruebas puede ser diferente a la que requiere su aplicación).
En resumen, mantenga el código de solo prueba fuera de su clase.
La comparación simple superficial de propiedades usando la reflexión debería ser suficiente para la mayoría de las clases, aunque es posible que deba recurrir si sus objetos tienen propiedades complejas. Si sigue las referencias, tenga cuidado con las referencias circulares o similares.
Astuto
He intentado varios enfoques mencionados aquí. La mayoría implica serializar sus objetos y hacer una comparación de cadenas. Aunque es súper fácil y, en general, es muy efectivo, he descubierto que se queda corto cuando tienes una falla y se informa algo como esto:
Expected string length 2326 but was 2342. Strings differ at index 1729.
Averiguar dónde están las diferencias es un dolor, por decir lo menos.
Con las comparaciones de gráficos de objetos de FluentAssertions (es decir, a.ShouldBeEquivalentTo(b)
), recuperas esto:
Expected property Name to be "Foo" but found "Bar"
Eso es mucho mejor. Obtén FluentAssertions ahora, te alegrarás más tarde (y si votaste positivamente esto, también devolviste la respuesta de dkl donde se sugirió FluentAsertions por primera vez).
La solución JSON de Max Wikstrom (arriba) tiene más sentido para mí, es corta, limpia y lo más importante, funciona. Personalmente, aunque preferiría implementar la conversión JSON como un método separado y volver a colocar la afirmación dentro de la prueba de la unidad ...
MÉTODO DE AYUDA:
public string GetObjectAsJson(object obj)
{
System.Web.Script.Serialization.JavaScriptSerializer oSerializer = new System.Web.Script.Serialization.JavaScriptSerializer();
return oSerializer.Serialize(obj);
}
PRUEBA DE UNIDAD :
public void GetDimensionsFromImageTest()
{
Image Image = new Bitmap(10, 10);
ImageHelpers_Accessor.ImageDimensions expected = new ImageHelpers_Accessor.ImageDimensions(10,10);
ImageHelpers_Accessor.ImageDimensions actual;
actual = ImageHelpers_Accessor.GetDimensionsFromImage(Image);
/*USING IT HERE >>>*/
Assert.AreEqual(GetObjectAsJson(expected), GetObjectAsJson(actual));
}
FYI - Puede que necesite agregar una referencia a System.Web.Extensions en su solución.
Me basaría en la respuesta de @Juanma. Sin embargo, creo que esto no debería implementarse con las afirmaciones de prueba unitaria. Esta es una utilidad que podría ser utilizada en algunas circunstancias por un código que no sea de prueba.
Escribí un artículo sobre el tema http://timoch.com/blog/2013/06/unit-test-equality-is-not-domain-equality/
Mi propuesta es la siguiente:
/// <summary>
/// Returns the names of the properties that are not equal on a and b.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns>An array of names of properties with distinct
/// values or null if a and b are null or not of the same type
/// </returns>
public static string[] GetDistinctProperties(object a, object b) {
if (object.ReferenceEquals(a, b))
return null;
if (a == null)
return null;
if (b == null)
return null;
var aType = a.GetType();
var bType = b.GetType();
if (aType != bType)
return null;
var props = aType.GetProperties();
if (props.Any(prop => prop.GetIndexParameters().Length != 0))
throw new ArgumentException("Types with index properties not supported");
return props
.Where(prop => !Equals(prop.GetValue(a, null), prop.GetValue(b, null)))
.Select(prop => prop.Name).ToArray();
}
Usando esto con NUnit
Expect(ReflectionUtils.GetDistinctProperties(tile, got), Empty);
produce el siguiente mensaje en desajuste.
Expected: <empty>
But was: < "MagmaLevel" >
at NUnit.Framework.Assert.That(Object actual, IResolveConstraint expression, String message, Object[] args)
at Undermine.Engine.Tests.TileMaps.BasicTileMapTests.BasicOperations() in BasicTileMapTests.cs: line 29
No anule Iguales solo para fines de prueba. Es tedioso y afecta la lógica del dominio. En lugar,
Use JSON para comparar los datos del objeto
Sin lógica adicional en sus objetos. No hay tareas adicionales para las pruebas.
Solo usa este método simple:
public static void AreEqualByJson(object expected, object actual)
{
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
var expectedJson = serializer.Serialize(expected);
var actualJson = serializer.Serialize(actual);
Assert.AreEqual(expectedJson, actualJson);
}
Parece que funciona bien. La información de resultados del corredor de prueba mostrará la comparación de cadenas JSON (el gráfico de objetos) incluido para que pueda ver directamente lo que está mal.
¡También ten en cuenta! Si tiene objetos complejos más grandes y solo quiere comparar partes de ellos, puede ( usar LINQ para datos de secuencia ) crear objetos anónimos para usar con el método anterior.
public void SomeTest()
{
var expect = new { PropA = 12, PropB = 14 };
var sut = loc.Resolve<SomeSvc>();
var bigObjectResult = sut.Execute(); // This will return a big object with loads of properties
AssExt.AreEqualByJson(expect, new { bigObjectResult.PropA, bigObjectResult.PropB });
}
Otra opción es escribir una restricción personalizada implementando la clase Constraint
abstracta de NUnit. Con una clase de ayuda para proporcionar un poco de azúcar sintáctico, el código de prueba resultante es agradablemente breve y legible, por ejemplo
Assert.That( LeftObject, PortfolioState.Matches( RightObject ) );
Para un ejemplo extremo, considere la clase que tiene miembros ''de solo lectura'', no es IEquatable
, y no podría cambiar la clase bajo prueba, incluso si quisiera:
public class Portfolio // Somewhat daft class for pedagogic purposes...
{
// Cannot be instanitated externally, instead has two ''factory'' methods
private Portfolio(){ }
// Immutable properties
public string Property1 { get; private set; }
public string Property2 { get; private set; } // Cannot be accessed externally
public string Property3 { get; private set; } // Cannot be accessed externally
// ''Factory'' method 1
public static Portfolio GetPortfolio(string p1, string p2, string p3)
{
return new Portfolio()
{
Property1 = p1,
Property2 = p2,
Property3 = p3
};
}
// ''Factory'' method 2
public static Portfolio GetDefault()
{
return new Portfolio()
{
Property1 = "{{NONE}}",
Property2 = "{{NONE}}",
Property3 = "{{NONE}}"
};
}
}
El contrato para la clase Constraint
requiere que se sobrescriban los WriteDescriptionTo
y la WriteDescriptionTo
(en el caso de una discrepancia, una narrativa para el valor esperado), pero también WriteActualValueTo
(narración del valor real):
public class PortfolioEqualityConstraint : Constraint
{
Portfolio expected;
string expectedMessage = "";
string actualMessage = "";
public PortfolioEqualityConstraint(Portfolio expected)
{
this.expected = expected;
}
public override bool Matches(object actual)
{
if ( actual == null && expected == null ) return true;
if ( !(actual is Portfolio) )
{
expectedMessage = "<Portfolio>";
actualMessage = "null";
return false;
}
return Matches((Portfolio)actual);
}
private bool Matches(Portfolio actual)
{
if ( expected == null && actual != null )
{
expectedMessage = "null";
expectedMessage = "non-null";
return false;
}
if ( ReferenceEquals(expected, actual) ) return true;
if ( !( expected.Property1.Equals(actual.Property1)
&& expected.Property2.Equals(actual.Property2)
&& expected.Property3.Equals(actual.Property3) ) )
{
expectedMessage = expected.ToStringForTest();
actualMessage = actual.ToStringForTest();
return false;
}
return true;
}
public override void WriteDescriptionTo(MessageWriter writer)
{
writer.WriteExpectedValue(expectedMessage);
}
public override void WriteActualValueTo(MessageWriter writer)
{
writer.WriteExpectedValue(actualMessage);
}
}
Además de la clase de ayuda:
public static class PortfolioState
{
public static PortfolioEqualityConstraint Matches(Portfolio expected)
{
return new PortfolioEqualityConstraint(expected);
}
public static string ToStringForTest(this Portfolio source)
{
return String.Format("Property1 = {0}, Property2 = {1}, Property3 = {2}.",
source.Property1, source.Property2, source.Property3 );
}
}
Ejemplo de uso:
[TestFixture]
class PortfolioTests
{
[Test]
public void TestPortfolioEquality()
{
Portfolio LeftObject
= Portfolio.GetDefault();
Portfolio RightObject
= Portfolio.GetPortfolio("{{GNOME}}", "{{NONE}}", "{{NONE}}");
Assert.That( LeftObject, PortfolioState.Matches( RightObject ) );
}
}
Prefiero no anular Igual solo para habilitar las pruebas. No olvide que si anula Equals, también debe anular GetHashCode o puede obtener resultados inesperados si está utilizando sus objetos en un diccionario, por ejemplo.
Me gusta el enfoque de reflexión anterior, ya que se ocupa de la adición de propiedades en el futuro.
Para una solución rápida y sencilla, sin embargo, a menudo es más fácil crear un método auxiliar que pruebe si los objetos son iguales o implementar IEqualityComparer en una clase que mantengas privada para tus pruebas. Al utilizar la solución IEqualityComparer, no necesita preocuparse por la implementación de GetHashCode. Por ejemplo:
// Sample class. This would be in your main assembly.
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// Unit tests
[TestFixture]
public class PersonTests
{
private class PersonComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
if (x == null && y == null)
{
return true;
}
if (x == null || y == null)
{
return false;
}
return (x.Name == y.Name) && (x.Age == y.Age);
}
public int GetHashCode(Person obj)
{
throw new NotImplementedException();
}
}
[Test]
public void Test_PersonComparer()
{
Person p1 = new Person { Name = "Tom", Age = 20 }; // Control data
Person p2 = new Person { Name = "Tom", Age = 20 }; // Same as control
Person p3 = new Person { Name = "Tom", Age = 30 }; // Different age
Person p4 = new Person { Name = "Bob", Age = 20 }; // Different name.
Assert.IsTrue(new PersonComparer().Equals(p1, p2), "People have same values");
Assert.IsFalse(new PersonComparer().Equals(p1, p3), "People have different ages.");
Assert.IsFalse(new PersonComparer().Equals(p1, p4), "People have different names.");
}
}
Pruebe la biblioteca FluentAsertions:
dto.ShouldHave(). AllProperties().EqualTo(customer);
http://www.fluentassertions.com/
También se puede instalar usando NuGet.
Si no puede anular Igualdad por cualquier motivo, puede crear un método de ayuda que recorre las propiedades públicas por reflexión y afirme cada propiedad. Algo como esto:
public static class AssertEx
{
public static void PropertyValuesAreEquals(object actual, object expected)
{
PropertyInfo[] properties = expected.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
object expectedValue = property.GetValue(expected, null);
object actualValue = property.GetValue(actual, null);
if (actualValue is IList)
AssertListsAreEquals(property, (IList)actualValue, (IList)expectedValue);
else if (!Equals(expectedValue, actualValue))
Assert.Fail("Property {0}.{1} does not match. Expected: {2} but was: {3}", property.DeclaringType.Name, property.Name, expectedValue, actualValue);
}
}
private static void AssertListsAreEquals(PropertyInfo property, IList actualList, IList expectedList)
{
if (actualList.Count != expectedList.Count)
Assert.Fail("Property {0}.{1} does not match. Expected IList containing {2} elements but was IList containing {3} elements", property.PropertyType.Name, property.Name, expectedList.Count, actualList.Count);
for (int i = 0; i < actualList.Count; i++)
if (!Equals(actualList[i], expectedList[i]))
Assert.Fail("Property {0}.{1} does not match. Expected IList with element {1} equals to {2} but was IList with element {1} equals to {3}", property.PropertyType.Name, property.Name, expectedList[i], actualList[i]);
}
}
Simplemente instale ExpectedObjects desde Nuget, puede comparar fácilmente el valor de propiedad de dos objetos, el valor de cada objeto de la colección, el valor de dos objetos compuestos y el valor de propiedad de comparación parcial por tipo anónimo.
Tengo algunos ejemplos en github: https://github.com/hatelove/CompareObjectEquals
Aquí hay algunos ejemplos que contienen escenarios de comparación de objetos:
[TestMethod]
public void Test_Person_Equals_with_ExpectedObjects()
{
//use extension method ToExpectedObject() from using ExpectedObjects namespace to project Person to ExpectedObject
var expected = new Person
{
Id = 1,
Name = "A",
Age = 10,
}.ToExpectedObject();
var actual = new Person
{
Id = 1,
Name = "A",
Age = 10,
};
//use ShouldEqual to compare expected and actual instance, if they are not equal, it will throw a System.Exception and its message includes what properties were not match our expectation.
expected.ShouldEqual(actual);
}
[TestMethod]
public void Test_PersonCollection_Equals_with_ExpectedObjects()
{
//collection just invoke extension method: ToExpectedObject() to project Collection<Person> to ExpectedObject too
var expected = new List<Person>
{
new Person { Id=1, Name="A",Age=10},
new Person { Id=2, Name="B",Age=20},
new Person { Id=3, Name="C",Age=30},
}.ToExpectedObject();
var actual = new List<Person>
{
new Person { Id=1, Name="A",Age=10},
new Person { Id=2, Name="B",Age=20},
new Person { Id=3, Name="C",Age=30},
};
expected.ShouldEqual(actual);
}
[TestMethod]
public void Test_ComposedPerson_Equals_with_ExpectedObjects()
{
//ExpectedObject will compare each value of property recursively, so composed type also simply compare equals.
var expected = new Person
{
Id = 1,
Name = "A",
Age = 10,
Order = new Order { Id = 91, Price = 910 },
}.ToExpectedObject();
var actual = new Person
{
Id = 1,
Name = "A",
Age = 10,
Order = new Order { Id = 91, Price = 910 },
};
expected.ShouldEqual(actual);
}
[TestMethod]
public void Test_PartialCompare_Person_Equals_with_ExpectedObjects()
{
//when partial comparing, you need to use anonymous type too. Because only anonymous type can dynamic define only a few properties should be assign.
var expected = new
{
Id = 1,
Age = 10,
Order = new { Id = 91 }, // composed type should be used anonymous type too, only compare properties. If you trace ExpectedObjects''s source code, you will find it invoke config.IgnoreType() first.
}.ToExpectedObject();
var actual = new Person
{
Id = 1,
Name = "B",
Age = 10,
Order = new Order { Id = 91, Price = 910 },
};
// partial comparing use ShouldMatch(), rather than ShouldEqual()
expected.ShouldMatch(actual);
}
Referencia:
Stringify y compara dos cadenas
Assert.AreEqual (JSON.stringify (LeftObject), JSON.stringify (RightObject))
Terminé escribiendo una fábrica de expresiones simples:
public static class AllFieldsEqualityComprision<T>
{
public static Comparison<T> Instance { get; } = GetInstance();
private static Comparison<T> GetInstance()
{
var type = typeof(T);
ParameterExpression[] parameters =
{
Expression.Parameter(type, "x"),
Expression.Parameter(type, "y")
};
var result = type.GetProperties().Aggregate<PropertyInfo, Expression>(
Expression.Constant(true),
(acc, prop) =>
Expression.And(acc,
Expression.Equal(
Expression.Property(parameters[0], prop.Name),
Expression.Property(parameters[1], prop.Name))));
var areEqualExpression = Expression.Condition(result, Expression.Constant(0), Expression.Constant(1));
return Expression.Lambda<Comparison<T>>(areEqualExpression, parameters).Compile();
}
}
y solo úsalo:
Assert.That(
expectedCollection,
Is.EqualTo(actualCollection)
.Using(AllFieldsEqualityComprision<BusinessCategoryResponse>.Instance));
Es muy útil ya que tengo que comparar la colección de dichos objetos. Y puedes usar esto para comparar en otro lado :)
Aquí está la esencia con el ejemplo: https://gist.github.com/Pzixel/b63fea074864892f9aba8ffde312094f
Las restricciones de propiedad , agregadas en NUnit 2.4.2, permiten una solución que es más legible que la original de OP, y produce mensajes de falla mucho mejores. No es de ninguna manera genérico, pero si no necesitas hacerlo para muchas clases, es una solución muy adecuada.
Assert.That(ActualObject, Has.Property("Prop1").EqualTo(ExpectedObject.Prop1)
& Has.Property("Prop2").EqualTo(ExpectedObject.Prop2)
& Has.Property("Prop3").EqualTo(ExpectedObject.Prop3)
// ...
No es tan general como implementar Equals
pero da un mensaje de falla mucho mejor que
Assert.AreEqual(ExpectedObject, ActualObject);
https://github.com/kbilsted/StatePrinter ha sido escrito específicamente para volcar gráficos de objetos a la representación de cadenas con el objetivo de escribir pruebas unitarias fáciles.
- Viene con los métodos de Assert que dan como resultado una copia fácil y fácil de copiar y pegar en la prueba para corregirla.
- Permite que unittest sea automáticamente reescrito
- Se integra con todos los marcos de prueba de la unidad
- A diferencia de la serialización JSON, las referencias circulares son compatibles
- Puede filtrar fácilmente, por lo que solo partes de tipos se vuelcan
Dado
class A
{
public DateTime X;
public DateTime Y { get; set; }
public string Name;
}
Puede hacerlo de una manera segura y utilizando la autocompletación de visual studio para incluir o excluir campos.
var printer = new Stateprinter();
printer.Configuration.Projectionharvester().Exclude<A>(x => x.X, x => x.Y);
var sut = new A { X = DateTime.Now, Name = "Charly" };
var expected = @"new A(){ Name = ""Charly""}";
printer.Assert.PrintIsSame(expected, sut);