Pourquoi cette assertion lève-t-elle une exception de format lors de la comparaison de structures?

94

J'essaye d'affirmer l'égalité de deux System.Drawing.Sizestructures, et j'obtiens une exception de format au lieu de l'échec d'assertion attendu.

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

Est-ce un comportement prévu? Est-ce que je fais quelque chose de mal ici?

Kyle
la source
avez-vous essayé d'avoir Assert.AreEqual(struct1, struct2, string.Format("Failed expected {0} actually is {1}, struct1.ToString (), struct2.ToString ())) `?
DiskJunky
Cela fonctionne très bien; Cependant, je suis curieux de savoir pourquoi Assert.AreEqual () ne peut pas formater une chaîne avec des types de structure.
Kyle
@Kyle Par curiosité, ce n'est pas avec la version compatible Silverlight du framework Unit Testing, n'est-ce pas? Je peux le reproduire avec ces DLL (je n'ai pas encore essayé la version complète du framework .NET) EDIT: tant pis, testé avec les pleins aussi et a toujours échoué. :)
Chris Sinclair
@ChrisSinclair non, cela utilise n'importe quelle version de mstest fournie avec Visual Studio 2010 Ultimate. Le projet de test lui-même cible .NET Framework 4
Kyle
4
Je ne sais pas si vous vous en foutez, mais cela fonctionne bien dans NUnit. J'ai vu plus de "problèmes" comme ceux-ci dans MStest. NUnit semble un peu plus mature (du moins pour moi). +1 pour le poste
bas le

Réponses:

100

J'ai compris. Et oui, c'est un bug.

Le problème est qu'il y a deux niveaux de fonctionnement string.Formatici.

Le premier niveau de formatage est quelque chose comme:

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

Ensuite, nous utilisons string.Formatavec les paramètres que vous avez fournis:

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

(De toute évidence, des cultures sont fournies, et une sorte de désinfection ... mais pas assez.)

Cela semble correct - à moins que les valeurs attendues et réelles elles-mêmes se retrouvent avec des accolades après avoir été converties en chaîne - ce qu'elles font Size. Par exemple, votre première taille finit par être convertie en:

{Width=0, Height=0}

Ainsi, le deuxième niveau de formatage est quelque chose comme:

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

... et c'est ce qui échoue. Aie.

En effet, nous pouvons le prouver très facilement en trompant le formatage pour utiliser nos paramètres pour les parties attendues et réelles:

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

Le résultat est:

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

Clairement cassé, car nous ne nous attendions pas, fooni la valeur réelle bar!

Fondamentalement, c'est comme une attaque par injection SQL, mais dans le contexte un peu moins effrayant de string.Format.

Pour contourner le problème, vous pouvez utiliser string.Formatcomme le suggère StriplingWarrior. Cela évite que le deuxième niveau de formatage soit effectué sur le résultat du formatage avec les valeurs réelles / attendues.

Jon Skeet
la source
Merci pour la réponse détaillée Jon! J'ai fini par utiliser le travail de StriplingWarriors.
Kyle
1
Pas d' %*néquivalent? :(
Tom Hawtin - tackline
Quelqu'un a-t-il soumis un rapport de bogue pour cela?
Kevin le
@Kevin: Ouais - bien qu'en interne, je ne suis donc pas sûr que les progrès seront visibles publiquement tant qu'ils ne seront pas corrigés.
Jon Skeet
1
@Kevin J'en ai mis un dans MS aussi une fois qu'un bug a été confirmé. connect.microsoft.com/VisualStudio/feedback/details/779528/… si vous souhaitez le suivre publiquement.
Kyle
43

Je pense que vous avez trouvé un bug.

Cela fonctionne (lève une exception assert):

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

Et cela fonctionne (affiche le message):

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

Mais cela ne fonctionne pas (lance un FormatException):

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

Je ne vois aucune raison pour laquelle ce serait un comportement attendu. Je soumettrais un rapport de bogue. En attendant, voici une solution de contournement:

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

Je suis d'accord avec @StriplingWarrior que cela semble effectivement être un bogue avec la méthode Assert.AreEqual () sur au moins 2 surcharges. Comme StiplingWarrior l'a déjà souligné, ce qui suit échoue;

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

J'ai fait un peu d'expérimentation à ce sujet pour être un peu plus explicite dans l'utilisation du code. Ce qui suit ne fonctionne pas non plus;

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

Et

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

Cela m'a fait réfléchir. System.Drawing.Size est une structure. Et les objets? La liste des param ne précise que la liste après le stringmessage est params object[]. Techniquement, les structures yes sont des objets ... mais des types d'objets spéciaux , c'est-à-dire des types valeur. Je pense que c'est là que réside le bogue. Si nous utilisons notre propre objet avec un usage similaire et structure Size, ce qui suit en fait fait le travail;

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
la source
1
Le problème n'est pas de savoir si c'est classou struct, mais si la ToStringvaleur contient des accolades ressemblant à un String.Format.
Jean Hominal
3

Je pense que la première affirmation est incorrecte.

Utilisez plutôt ceci:

Assert.AreEqual(struct1, 
                struct2, 
                string.Format("Failed expected {0} actually is {1}", struct1, struct2));
Polaris
la source
D'après la documentation, je devrais pouvoir appeler AreEqual avec une chaîne formatée. msdn.microsoft.com/en-us/library/ms243436%28v=vs.100%29.aspx , en particulier les paramètres Type: System.Object [] Tableau de paramètres à utiliser lors du formatage du message.
Kyle