¿Por qué esta afirmación arroja una excepción de formato al comparar estructuras?

94

Estoy tratando de afirmar la igualdad de dos System.Drawing.Sizeestructuras y obtengo una excepción de formato en lugar del error de afirmación esperado.

[TestMethod]
public void AssertStructs()
{
    var struct1 = new Size(0, 0);
    var struct2 = new Size(1, 1);

    //This throws a format exception, "System.FormatException: Input string was not in a correct format."
    Assert.AreEqual(struct1, struct2, "Failed. Expected {0}, actually it is {1}", struct1, struct2); 

    //This assert fails properly, "Failed. Expected {Width=0, Height=0}, actually it is {Width=1, Height=1}".
    Assert.AreEqual(struct1, struct2, "Failed. Expected " + struct1 + ", actually it is " + struct2); 
}

¿Es este comportamiento previsto? ¿Estoy haciendo algo mal aquí?

Kyle
fuente
¿ha intentado tener Assert.AreEqual(struct1, struct2, string.Format("Failed expected {0} actually is {1}, struct1.ToString (), struct2.ToString ())) `?
DiskJunky
Eso funciona bien; sin embargo, tengo curiosidad por saber por qué Assert.AreEqual () no puede formatear una cadena con tipos de estructura.
Kyle
@Kyle Por curiosidad, esto no es con la versión compatible con Silverlight del marco de pruebas unitarias, ¿verdad? Puedo reproducirlo con esas DLL (aún no he probado la versión completa de .NET framework) EDITAR: no importa, también probé con las completas y aún así fallé. :)
Chris Sinclair
@ChrisSinclair no, esto está usando cualquier versión de mstest que viene con Visual Studio 2010 ultimate. El proyecto de prueba en sí está dirigido a .NET Framework 4
Kyle
4
No estoy seguro de si te importa un carajo, pero esto funciona bien en NUnit. He visto más "problemas" como estos en MStest. NUnit parece un poco más maduro (al menos para mí). +1 para la publicación
bas

Respuestas:

100

Lo tengo. Y sí, es un error.

El problema es que hay dos niveles de string.Formatacción aquí.

El primer nivel de formateo es algo como:

string template  = string.Format("Expected: {0}; Actual: {1}; Message: {2}",
                                 expected, actual, message);

Luego usamos string.Formatcon los parámetros que ha proporcionado:

string finalMessage = string.Format(template, parameters);

(Obviamente, se están proporcionando cultivos y algún tipo de desinfección ... pero no lo suficiente).

Eso se ve bien, a menos que los valores esperados y reales terminen con llaves, después de convertirse en una cadena, para lo cual sirven Size. Por ejemplo, su primer tamaño termina convirtiéndose en:

{Width=0, Height=0}

Entonces, el segundo nivel de formato es algo como:

string.Format("Expected: {Width=0, Height=0}; Actual: {Width=1, Height=1 }; " +
              "Message = Failed expected {0} actually is {1}", struct1, struct2);

... y eso es lo que falla. Ay.

De hecho, podemos probar esto muy fácilmente engañando al formato para que use nuestros parámetros para las partes esperadas y reales:

var x = "{0}";
var y = "{1}";
Assert.AreEqual<object>(x, y, "What a surprise!", "foo", "bar");

El resultado es:

Assert.AreEqual failed. Expected:<foo>. Actual:<bar>. What a surprise!

Claramente roto, ya que no lo esperábamos fooni el valor real bar.

Básicamente, esto es como un ataque de inyección SQL, pero en el contexto bastante menos aterrador de string.Format.

Como solución alternativa, puede utilizarlo string.Formatcomo sugiere StriplingWarrior. Eso evita que se realice el segundo nivel de formateo en el resultado del formateo con los valores reales / esperados.

Jon Skeet
fuente
¡Gracias por la respuesta detallada Jon! Terminé usando el trabajo de StriplingWarriors.
Kyle
1
¿No tiene %*nequivalente? :(
Tom Hawtin - tackline
¿Alguien ha enviado un informe de error para esto?
Kevin
@Kevin: Sí, aunque internamente, no estoy seguro de si el progreso será visible públicamente hasta que se solucione.
Jon Skeet
1
@Kevin También puse uno en MS una vez que se confirmó un error. connect.microsoft.com/VisualStudio/feedback/details/779528/… si desea realizar un seguimiento público.
Kyle
43

Creo que has encontrado un error.

Esto funciona (lanza una excepción de aserción):

var a = 1;
var b = 2;
Assert.AreEqual(a, b, "Not equal {0} {1}", a, b);

Y esto funciona (genera el mensaje):

var a = new{c=1};
var b = new{c=2};
Console.WriteLine(string.Format("Not equal {0} {1}", a, b));

Pero esto no funciona (lanza un FormatException):

var a = new{c=1};
var b = new{c=2};
Assert.AreEqual(a, b, "Not equal {0} {1}", a, b);

No puedo pensar en ninguna razón por la que este comportamiento sería el esperado. Enviaría un informe de errores. Mientras tanto, aquí hay una solución:

var a = new{c=1};
var b = new{c=2};
Assert.AreEqual(a, b, string.Format("Not equal {0} {1}", a, b));
StriplingGuerrero
fuente
5

Estoy de acuerdo con @StriplingWarrior en que esto parece ser un error con el método Assert.AreEqual () en al menos 2 sobrecargas. Como ya ha señalado StiplingWarrior, lo siguiente falla;

var a = new { c = 1 };
var b = new { c = 2 };
Assert.AreEqual(a, b, "Not equal {0} {1}", a, b);

He estado experimentando un poco más sobre esto para ser un poco más explícito en el uso del código. Lo siguiente tampoco funciona;

// specify variable data type rather than "var"...no effect, still fails
Size a = new Size(0, 0);
Size b = new Size(1, 1);
Assert.AreEqual(a, b, "Not equal {0} {1}", a, b);

Y

// specify variable data type and name the type on the generic overload of AreEqual()...no effect, still fails
Size a = new Size(0, 0);
Size b = new Size(1, 1);
Assert.AreEqual<Size>(a, b, "Not equal {0} {1}", a, b);

Esto me hizo pensar. System.Drawing.Size es una estructura. ¿Y los objetos? La lista parámetro no se especifica que la lista después de que el stringmensaje es params object[]. Técnicamente, sí, las estructuras son objetos ... pero tipos especiales de objetos, es decir, tipos de valor. Creo que aquí es donde está el error. Si utilizamos nuestro propio objeto con un uso y estructura similares a Sizela siguiente realidad hace el trabajo;

private class MyClass
{
    public MyClass(int width, int height)
        : base()
    { Width = width; Height = height; }

    public int Width { get; set; }
    public int Height { get; set; }
}

[TestMethod]
public void TestMethod1()
{
    var test1 = new MyClass(0, 0);
    var test2 = new MyClass(1, 1);
    Assert.AreEqual(test1, test2, "Show me A [{0}] and B [{1}]", test1, test2);
}
DiskJunky
fuente
1
El problema no es si es classo struct, sino si el ToStringvalor contiene llaves que se parecen a un String.Format.
Jean Hominal
3

Creo que la primera afirmación es incorrecta.

Use esto en su lugar:

Assert.AreEqual(struct1, 
                struct2, 
                string.Format("Failed expected {0} actually is {1}", struct1, struct2));
estrella polar
fuente
De acuerdo con la documentación, debería poder llamar a AreEqual con una cadena formateada. msdn.microsoft.com/en-us/library/ms243436%28v=vs.100%29.aspx , específicamente los parámetros Tipo: System.Object [] Una matriz de parámetros para usar al formatear el mensaje.
Kyle