Postanowiłem wreszcie nauczyć się tych nieszczęsnych testów...
Ćwiczebny problem, który mam nadzieję rozwinąć do czegoś sensownego i większego: Chatroom.
Krok pierwszy: Chatbox siedzący tylko w przeglądarce, user wpisuje swój nick i wiadomość i wiadomości pojawiają się jedna po drugiej. Nic użytecznego, ale już można dopisać testy.
Plik z takim minimalistycznym chatboxem: 40 linii.
Z kolei plik z testami do powyższego: 158 linii!
A to i tak nie wszystko, bo nie testuję jeszcze np. górnego ograniczenia na długość nicku i górnego ograniczenia na długość postu (ogarnąłem się, że są z tym pewne subtelności, których nie zauważyłem dotychczas - zamierzam się tym zająć wtedy, kiedy dodam zapisywanie wiadomości do bazy danych na serwerze(*))
Czy taka sytacja jest OK?Wydaje mi się tu być coś dziwnego. Z drugiej strony każdy test testuje jakąś założoną funkcjonalność, więc jakby żaden nie jest nadprogramowy?
To prawda, jest spora duplikacja kodu w testach. Możnaby ją usunąć np. w ten sposób, by wprowadzić pomocnicze metody PostMessage
czy AssertMessageEqual
. Ale nie zrobiłem tego świadomie, dlatego, że gdzieś wyczytałem, że do testów odnoszą się zupełnie inne zasady, niż do "normalnego" kodu: testy winny być w największym możliwym stopniu pozbawione logiki i napisane najprościej, jak to możliwe, by można było łatwo zobaczyć powód, dlaczego dany test nie przechodzi patrząc tylko na jego kod (a nie jeszcze na kod iluśtam pomocniczych metod i klas) i by nie musieć pisać testów testujących testy.
Ponieważ testy zawsze były moją piętą achillesową, czy mogę się zapytać, czy poniższe jest OK, czy jednak z braku wiedzy/doświadczenia robię coś bardzo złego i tego nie widzę?
(*) Idea ograniczenia [StringLength(maximumLength: 16)]
na długość nicku jest taka, by później można było to wrzucić do bazy do kolumny typu CHAR[16]
. Ale to tak nie działa, bo CHAR[16]
to jest ilość bajtów w UTF-8, zaś [StringLength]
wymusza ograniczenie na ilość znaków, a tak naprawdę to chyba na ilość 16-bitowych char
ów w kodowaniu UTF-16. Więc tak to nie zadziała. Chyba będę musiał przeliczyć na kartce, ile bajtów UTF-8 może w pesymistycznym przypadku zająć string o długości 16 dwubajtowych char
ów i testować, czy username wprowadzony do chatboxa po konwersji do UTF-8 może przebić tę wartość. Może więc jednak źle robię, bo zostawiłem nieotestowany kod, o którym wiem, że będzie musiał się zmienić?
Plik App.razor
:
<EditForm Model="@pendingMessage" OnValidSubmit="Post">
<label>Chose your name: <InputText class="username-input" @bind-Value="pendingMessage.UserName"/></label>
<ol>
@foreach(Message message in messages)
{
<li class="chat-message">
<b class="chat-message-author">@message.UserName</b>: <span class="chat-message-content">@message.Content</span>
</li>
}
</ol>
<label>Type your message: <InputText class="chat-message-content-input" @bind-Value="pendingMessage.Content"/></label> <button class="post-chat-message-button" type="submit">Send</button>
<DataAnnotationsValidator />
<ValidationSummary />
</EditForm>
@code{
Message pendingMessage = new();
List<Message> messages = new();
void Post()
{
messages.Add(new Message { UserName = pendingMessage.UserName, Content = pendingMessage.Content });
pendingMessage.Content = "";
}
class Message
{
[Required]
[StringLength(maximumLength: 16, MinimumLength = 1)]
public string? UserName { get; set; }
[Required]
[StringLength(maximumLength: 160, MinimumLength = 1)]
public string? Content { get; set; }
}
}
I plik AppTests.razor
:
@inherits TestContext
@code {
[Fact]
public void CanAddAMessage()
{
IRenderedFragment cut = RenderComponent<App>();
IElement usernameInput = cut.Find(".username-input");
IElement contentInput = cut.Find(".chat-message-content-input");
IElement sendButton = cut.Find(".post-chat-message-button");
string expectedUserName = "Master Cock";
string expectedMessageContent = "You may be a treasure to men that prize you, but for me I would rather have a single barley-corn than a peck of pearls.";
usernameInput.Change(expectedUserName);
contentInput.Change(expectedMessageContent);
sendButton.Click();
string actualUserName = cut.Find(".chat-message-author").TextContent;
string actualMessageContent = cut.Find(".chat-message-content").TextContent;
Assert.Equal(expectedUserName, actualUserName);
Assert.Equal(expectedMessageContent, actualMessageContent);
}
[Fact]
public void ChatIsEmptyInTheBeginning()
{
IRenderedFragment cut = RenderComponent<App>();
Assert.Empty(cut.FindAll(".chat-message"));
}
[Fact]
public void AddingAMessageAddsOnlyOneMessage()
{
IRenderedFragment cut = RenderComponent<App>();
IElement usernameInput = cut.Find(".username-input");
IElement contentInput = cut.Find(".chat-message-content-input");
IElement sendButton = cut.Find(".post-chat-message-button");
string expectedUserName = "Master Cock";
string expectedMessageContent = "Ho! ho! that's for me";
usernameInput.Change(expectedUserName);
contentInput.Change(expectedMessageContent);
sendButton.Click();
Assert.Single(cut.FindAll(".chat-message"));
}
[Fact]
public void SecondMessageAppendsToTheFirstOne()
{
IRenderedFragment cut = RenderComponent<App>();
IElement usernameInput = cut.Find(".username-input");
IElement contentInput = cut.Find(".chat-message-content-input");
IElement sendButton = cut.Find(".post-chat-message-button");
string firstExpectedUserName = "Wolf";
string firstExpectedMessageContent = "How dare you muddle the water from which I am drinking.";
usernameInput.Change(firstExpectedUserName);
contentInput.Change(firstExpectedMessageContent);
sendButton.Click();
string secondExpectedUserName = "Lambikin";
string secondExpectedMessageContent = "Nay, master, nay, if the water be muddy up there, I cannot be the cause of it, for it runs down from you to me.";
usernameInput.Change(secondExpectedUserName);
contentInput.Change(secondExpectedMessageContent);
sendButton.Click();
string firstActualUserName = cut.FindAll(".chat-message-author")[0].TextContent;
string firstActualMessageContent = cut.FindAll(".chat-message-content")[0].TextContent;
string secondActualUserName = cut.FindAll(".chat-message-author")[1].TextContent;
string secondActualMessageContent = cut.FindAll(".chat-message-content")[1].TextContent;
Assert.Equal(firstExpectedUserName, firstActualUserName);
Assert.Equal(firstExpectedMessageContent, firstActualMessageContent);
Assert.Equal(secondExpectedUserName, secondActualUserName);
Assert.Equal(secondExpectedMessageContent, secondActualMessageContent);
}
[Fact]
public void PostingMessageClearsNextMessageContent()
{
IRenderedFragment cut = RenderComponent<App>();
IElement usernameInput = cut.Find(".username-input");
IElement contentInput = cut.Find(".chat-message-content-input");
IElement sendButton = cut.Find(".post-chat-message-button");
string userName = "Wolf";
string messageContent = "There's my supper, if only I can find some excuse to seize it.";
usernameInput.Change(userName);
contentInput.Change(messageContent);
sendButton.Click();
Assert.Equal("", contentInput.GetAttribute("value"));
}
[Fact]
public void PostingMessageDoesNotChangeNextMessageUsername()
{
IRenderedFragment cut = RenderComponent<App>();
IElement usernameInput = cut.Find(".username-input");
IElement contentInput = cut.Find(".chat-message-content-input");
IElement sendButton = cut.Find(".post-chat-message-button");
string expectedUserName = "Wolf";
string messageContent = "I don't care, if it was not you, it was your father";
usernameInput.Change(expectedUserName);
contentInput.Change(messageContent);
sendButton.Click();
Assert.Equal(expectedUserName, usernameInput.GetAttribute("value"));
}
[Fact]
public void CannotPostEmptyMessages()
{
IRenderedFragment cut = RenderComponent<App>();
IElement usernameInput = cut.Find(".username-input");
IElement contentInput = cut.Find(".chat-message-content-input");
IElement sendButton = cut.Find(".post-chat-message-button");
string userName = "Dog";
string messageContent = "";
usernameInput.Change(userName);
contentInput.Change(messageContent);
sendButton.Click();
Assert.Empty(cut.FindAll(".chat-message"));
Assert.NotEmpty(cut.FindAll(".validation-message"));
}
[Fact]
public void CannotPostEmptyUsernames()
{
IRenderedFragment cut = RenderComponent<App>();
IElement usernameInput = cut.Find(".username-input");
IElement contentInput = cut.Find(".chat-message-content-input");
IElement sendButton = cut.Find(".post-chat-message-button");
string expectedUserName = "";
string messageContent = "Any excuse will serve a tyrant.";
usernameInput.Change(expectedUserName);
contentInput.Change(messageContent);
sendButton.Click();
Assert.Empty(cut.FindAll(".chat-message"));
Assert.NotEmpty(cut.FindAll(".validation-message"));
}
}