Dlaczego ta asercja zgłasza wyjątek formatu podczas porównywania struktur?

94

Próbuję zapewnić równość dwóch System.Drawing.Sizestruktur i otrzymuję wyjątek formatu zamiast oczekiwanego niepowodzenia potwierdzenia.

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

Czy to zamierzone zachowanie? Czy ja tu robię coś złego?

Kyle
źródło
czy próbowałeś mieć Assert.AreEqual(struct1, struct2, string.Format("Failed expected {0} actually is {1}struct1.ToString (), struct2.ToString ())) `?
DiskJunky
To działa dobrze; jestem jednak ciekawy, dlaczego Assert.AreEqual () nie może sformatować łańcucha z typami struktury.
Kyle
@Kyle Z ciekawości, to nie jest w przypadku zgodnej z Silverlight wersji frameworka testów jednostkowych, prawda? Mogę go odtworzyć za pomocą tych bibliotek DLL (nie wypróbowałem jeszcze pełnej wersji platformy .NET) EDYCJA: nieważne, testowane również z pełnymi bibliotekami i nadal nie udało się. :)
Chris Sinclair
@ChrisSinclair nie, to jest używana dowolna wersja Mstest, która jest dostarczana z Visual Studio 2010 ultimate. Sam projekt testowy jest skierowany do .NET Framework 4
Kyle
4
Nie jestem pewien, czy obchodzi cię to, ale to działa dobrze w NUnit. Widziałem więcej "problemów" takich jak te w MStest. NUnit wydaje się nieco bardziej dojrzały (przynajmniej dla mnie). +1 za post
bas

Odpowiedzi:

100

Mam to. I tak, to błąd.

Problem w tym, że string.Formatdzieje się tutaj na dwóch poziomach .

Pierwszy poziom formatowania jest coś takiego:

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

Następnie używamy string.Formatz podanymi parametrami:

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

(Oczywiście zapewniane są kultury i jakiś rodzaj sanityzacji ... ale to za mało).

Wygląda to dobrze - chyba że oczekiwane i rzeczywiste wartości same kończą się nawiasami klamrowymi po konwersji na ciąg - co robią Size. Na przykład Twój pierwszy rozmiar kończy się konwersją na:

{Width=0, Height=0}

Więc drugi poziom formatowania to coś takiego:

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

... i to właśnie zawodzi. Auć.

Rzeczywiście, możemy to naprawdę łatwo udowodnić, oszukując formatowanie, aby użyć naszych parametrów dla oczekiwanych i rzeczywistych części:

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

Wynik to:

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

Wyraźnie zepsuty, czego się nie spodziewaliśmy, fooani rzeczywista wartość bar!

Zasadniczo przypomina to atak polegający na wstrzyknięciu kodu SQL, ale w raczej mniej przerażającym kontekście string.Format.

Aby obejść ten problem, możesz użyć tego, string.Formatco sugeruje StriplingWarrior. Pozwala to uniknąć drugiego poziomu formatowania wykonywanego na wyniku formatowania z wartościami rzeczywistymi / oczekiwanymi.

Jon Skeet
źródło
Dzięki za szczegółową odpowiedź Jon! Skończyło się na tym, że użyłem pracy StriplingWarriors.
Kyle,
1
Brak %*nodpowiednika? :(
Tom Hawtin - tackline
Czy ktoś przesłał w tej sprawie raport o błędzie?
Kevin,
@Kevin: Tak - chociaż wewnętrznie, więc nie jestem pewien, czy postęp będzie widoczny publicznie, dopóki nie zostanie naprawiony.
Jon Skeet,
1
@Kevin Umieściłem jeden w MS, gdy został potwierdzony błąd. connect.microsoft.com/VisualStudio/feedback/details/779528/ ... jeśli chcesz śledzić to publicznie.
Kyle,
43

Myślę, że znalazłeś błąd.

To działa (zgłasza wyjątek assert):

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

I to działa (wyświetla komunikat):

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

Ale to nie działa (rzuca a FormatException):

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

Nie mogę wymyślić żadnego powodu, dla którego byłoby to oczekiwane zachowanie. Złożyłbym raport o błędzie. Tymczasem oto obejście:

var a = new{c=1};
var b = new{c=2};
Assert.AreEqual(a, b, string.Format("Not equal {0} {1}", a, b));
StriplingWarrior
źródło
5

Zgadzam się z @StriplingWarrior, że to rzeczywiście wygląda na błąd w metodzie Assert.AreEqual () przy co najmniej 2 przeciążeniach. Jak już zauważył StiplingWarrior, następujące działania zawodzą;

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

Zrobiłem trochę eksperymentów w tym zakresie, aby być bardziej wyraźnym w użyciu kodu. Poniższe również nie działa;

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

I

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

To zmusiło mnie do myślenia. System.Drawing.Size to struktura. A co z przedmiotami? Lista param ma określić, że lista po stringwiadomości wynosi params object[]. Technicznie rzecz biorąc, struktury tak obiektami ... ale specjalnymi rodzajami obiektów, tj. Typami wartości. Myślę, że w tym tkwi błąd. Jeśli używamy nasz własny obiekt o podobnej strukturze do użytkowania i Sizedodaje się faktycznie robi praca;

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
źródło
1
Problemem nie jest to, czy jest to classczy struct, ale czy ToStringwartość zawiera nawiasów klamrowych, które wyglądają jak String.Format.
Jean Hominal
3

Myślę, że pierwsze twierdzenie jest błędne.

Użyj tego zamiast tego:

Assert.AreEqual(struct1, 
                struct2, 
                string.Format("Failed expected {0} actually is {1}", struct1, struct2));
Polaris
źródło
Zgodnie z dokumentacją powinienem być w stanie wywołać AreEqual ze sformatowanym ciągiem. msdn.microsoft.com/en-us/library/ms243436%28v=vs.100%29.aspx , a konkretnie parametry Typ: System.Object [] Tablica parametrów używanych podczas formatowania wiadomości.
Kyle