Por que essa declaração lança uma exceção de formato ao comparar estruturas?

94

Estou tentando afirmar a igualdade de duas System.Drawing.Sizeestruturas e estou obtendo uma exceção de formato em vez da falha de declaração esperada.

[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); 
}

Este é o comportamento pretendido? Estou fazendo algo errado aqui?

Kyle
fonte
você já tentou ter Assert.AreEqual(struct1, struct2, string.Format("Failed expected {0} actually is {1}, struct1.ToString (), struct2.ToString ())) `?
DiskJunky
Isso funciona bem; no entanto, estou curioso para saber por que Assert.AreEqual () não pode formatar uma string com tipos de estrutura.
Kyle
@Kyle Por curiosidade, isso não é com a versão compatível do Silverlight da estrutura de teste de unidade, certo? Posso reproduzi-lo com aquelas DLLs (ainda não experimentei a versão completa do .NET framework) EDIT: deixa pra lá, testei com os completos também e ainda falhou. :)
Chris Sinclair
@ChrisSinclair não, isso é usar qualquer versão do mstest que vem com o Visual Studio 2010 ultimate. O projeto de teste em si tem como alvo o .NET Framework 4
Kyle
4
Não tenho certeza se você se importa, mas isso funciona bem no NUnit. Tenho visto mais "problemas" como esses no MStest. NUnit parece um pouco mais maduro (pelo menos para mim). +1 para o cargo
bas

Respostas:

100

Eu tenho isso E sim, é um bug.

O problema é que existem dois níveis de coisas string.Formatacontecendo aqui.

O primeiro nível de formatação é algo como:

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

Em seguida, usamos string.Formatcom os parâmetros que você forneceu:

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

(Obviamente, há culturas sendo fornecidas e algum tipo de higienização ... mas não o suficiente.)

Isso parece bom - a menos que os próprios valores esperados e reais terminem com colchetes, depois de serem convertidos em uma string - o que eles fazem Size. Por exemplo, seu primeiro tamanho acaba sendo convertido para:

{Width=0, Height=0}

Portanto, o segundo nível de formatação é algo como:

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

... e é isso que está falhando. Ai.

Na verdade, podemos provar isso facilmente enganando a formatação para usar nossos parâmetros para as partes esperadas e reais:

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

O resultado é:

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

Claramente quebrado, pois não esperávamos foonem o valor real bar!

Basicamente, é como um ataque de injeção de SQL, mas no contexto menos assustador de string.Format.

Como solução alternativa, você pode usar o string.Formatque StriplingWarrior sugere. Isso evita que o segundo nível de formatação seja executado no resultado da formatação com os valores reais / esperados.

Jon Skeet
fonte
Obrigado pela resposta detalhada Jon! Acabei usando uma solução alternativa para StriplingWarriors.
Kyle
1
Sem %*nequivalente? :(
Tom Hawtin - tackline
Alguém enviou um relatório de bug para isso?
Kevin
@Kevin: Sim - embora internamente, não tenho certeza se o progresso ficará publicamente visível até que seja corrigido.
Jon Skeet de
1
@Kevin Eu coloquei um no MS também, uma vez que foi confirmado um bug. connect.microsoft.com/VisualStudio/feedback/details/779528/… se você deseja rastreá-lo publicamente.
Kyle de
43

Acho que você encontrou um bug.

Isso funciona (lança uma exceção de declaração):

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

E isso funciona (gera a mensagem):

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

Mas isso não funciona (lança um FormatException):

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

Não consigo pensar em nenhuma razão para esse comportamento ser esperado. Eu enviaria um relatório de bug. Enquanto isso, aqui está uma solução alternativa:

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

Eu concordo com @StriplingWarrior que isso realmente parece ser um bug com o método Assert.AreEqual () em pelo menos 2 sobrecargas. Como StiplingWarrior já apontou, o seguinte falha;

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

Tenho feito algumas experiências para ser um pouco mais explícito no uso do código. O seguinte também não 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);

E

// 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);

Isso me fez pensar. System.Drawing.Size é uma estrutura. E quanto a objetos? A lista param não especificar que a lista após a stringmensagem é params object[]. Tecnicamente, sim, estruturas são objetos ... mas tipos especiais de objetos, ou seja, tipos de valor. Acho que é aí que está o bug. Se usarmos nosso próprio objeto com um uso e estrutura para semelhante Size, o seguinte realmente faz o trabalho;

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
fonte
1
O problema não é se é classou struct, mas se o ToStringvalor contém chaves que se parecem com um String.Format.
Jean Hominal
3

Acho que a primeira afirmação está incorreta.

Em vez disso, use:

Assert.AreEqual(struct1, 
                struct2, 
                string.Format("Failed expected {0} actually is {1}", struct1, struct2));
Polaris
fonte
De acordo com a documentação, devo ser capaz de chamar AreEqual com uma string formatada. msdn.microsoft.com/en-us/library/ms243436%28v=vs.100%29.aspx , especificamente os parâmetros Tipo: System.Object [] Uma matriz de parâmetros para usar ao formatar a mensagem.
Kyle