Testy wydajnościowe aplikacji webowych  

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2011-02-23

Pobierz kod źródłowy

 

Wprowadzenie

Visual Studio, oprócz wsparcia dla klasycznych testów jednostkowych, pozwala na tworzenie tzw. testów wydajnościowych web, które umożliwiają weryfikację wydajności aplikacji webowych. Oparte są one na mechanizmie nagrywania – przed uruchomieniem testów użytkownik nagrywa zapytania, wykorzystując zwykłą przeglądarkę internetową z zainstalowanym pluginem. Nagrywanie nie różni się więc niczym od standardowego przeglądania strony www.

Tworzenie testu

Najpierw należy stworzyć standardowy projekt do testów:

Rys. 1. Utworzenie projektu testów.

Kolejnym krokiem jest utworzenie konkretnego testu. W tym celu z menu kontekstowego Solution Explorer wybieramy Add->Web Performance Test. Po kliknięciu na test pojawi się okno służące do nagrywania ruchu:

Rys. 2. Nagrywanie ruchu.

Nagrywanie

  1. Klikamy w odnośnik Show Recorder (rys. 2).  Zostanie uruchomiona przeglądarka wraz z Web Test Recorder:


    Rys. 3. Przeglądarka Internet Explorer wraz z uruchomionym Web Test Recorder.

  2. Aby rozpocząć nagrywanie, wystarczy wpisać adres strony. Dla celów tego artykułu użyjemy www.bing.com wraz z wyszukiwaniem tekstu „ASP.NET”. Po załadowaniu strony pojawią się w Web Test Recorder adresy nagranych podstron:


    Rys. 4. Nagrywanie strony.

  3. Po nagraniu ruchu klikamy w przycisk Stop i przechodzimy do Visual Studio.


    Rys. 5. Nagrany test.

    Nagrany test zawiera kilka ważnych informacji. Przede wszystkim pokazuje listę wykonanych zapytań (korelacja), a ponadto, po rozwinięciu zapytań, można przejrzeć QueryString:


    Rys. 6. Parametry QueryString.

W powyższym oknie można również obejrzeć tzw. context parameter. Są to parametry dostępne w obrębie całej aplikacji, a nie tylko konkretnego zapytania – jak ma to miejsce w przypadku QueryString.

Parametryzacja

Nagrany test można skonfigurować i następnie wywoływać go wielokrotnie. W powyższym teście warto sparametryzować QueryString odpowiadający za treść wyszukiwanej frazy. Najpierw jednak należy dodać źródło danych zawierające parametry:

  1. Klikamy na teście i z menu kontekstowego wybieramy Add Data Source.


    Rys. 7. Nowe źródło danych.

  2. Wybieramy CSV File (Comma-separated values] – plik tekstowy, w którym kolejne wartości tego samego wiersza oddzielone są przecinkami.

  3. W kolejnym kroku wybieramy plik CSV zawierający listę słów kluczowych.


    Rys. 8. Określenie pliku źródłowego.

  4. Po kliknięciu w Finish pojawi się jeszcze zapytanie, czy dodać plik CSV do projektu – wyrażamy oczywiście zgodę.


    Rys. 9. Wyrażamy zgodę na dodanie pliku do projektu.

Następnie klikamy na odpowiednim QueryString. W okienku Properties – zamiast wartości zdefiniowanej na stałe – zaznaczamy źródło danych.


Rys. 10. Parametryzacja QueryString.

W tej chwili wartość QueryString została powiązana z plikiem CSV. Podczas wykonywania testów będziemy mogli przetestować wydajność strony, wykorzystując różne słowa kluczowe.

Ekstrakcja reguł

Ekstrakcja reguł pozwala na wyłuskanie różnych fragmentów strony www. Można m.in. wyekstrahować pola formularzy, atrybuty html, nagłówki http itp. Wydobyte wartości mogą służyć jako parametry wejściowe w następnych zapytaniach lub mogą posłużyć w późniejszych decyzjach biznesowych. Spróbujmy więc wydobyć jakiś nagłówek HTTP:

  1. klikamy na węźle www.bing.com, przechodzimy do Extraction rules i z menu kontekstowego wybieramy Add Extraction Rule:


    Rys. 11. Dodawanie nowej reguły ekstrakcji.

  2. zaznaczamy Extract Httpheader, a następnie wypełniamy pola Context Parameter name oraz Header Name. Pierwsze pole oznacza nazwę zmiennej, w której zostanie przechowana wyciągnięta wartość. Z kolei  do drugiego pola (Header Name) wpisujemy oczywiście nazwę nagłówka, który chcemy wydobyć:


    Rys. 12. Konfiguracja reguły ekstrakcji.

Zasady walidacji

Zasady walidacji pozwalają na sprawdzenie, czy strona została wygenerowana tak jak się spodziewamy. Walidację można przeprowadzać na postawie tagów html, atrybutów, zwróconego tekstu, adresu url, pól formularza itd. Przykładowo, po wyświetleniu strony z wynikami w Bing możemy sprawdzić, czy zwrócona odpowiedź zawiera tekst „Results”. Spróbujmy więc stworzyć taką regułę walidacji:

  1. w oknie testu klikamy na węźle Validation Rules i z menu kontekstowego wybieramy Add Validation Rule:


    Rys. 13. Okno reguł walidacyjnych.

  2. klikamy na opcji Find Text, a następnie w pole Find Text wpisujemy „Results”. Dodatkowo ustawiamy Ignore Case na True (wielkość liter nie będzie miała znaczenia):


    Rys. 14. Konfiguracja reguły walidacji.

Domyślnie reguła będzie uznana za zaliczoną, gdy wpisany tekst w polu Find Text zostanie odnaleziony w wygenerowanej stronie. Można również ustawić pole Pass If Text Found na false, jeśli chcemy, aby reguła nie została zaliczona w przypadku odnalezienia tekstu.

Transakcje

Transakcja definiuje ciąg czynności. Domyślnie w wynikach testu każde zapytanie będzie mierzone i przedstawiane osobno. W praktyce jednak często chcemy analizować ciąg zapytań jako jedną czynność. W takim przypadku musimy użyć transakcji, która definiuje po prostu serię zapytań. Dodanie transakcji jest bardzo proste:

  1. klikamy na teście i z menu kontekstowego wybieramy Add Transaction:


    Rys. 15. Tworzenie nowej transakcji.

  2. wpisujemy nazwę translacji i wybieramy przedział zapytań, które będzie ona obejmować:


    Rys. 16. Konfiguracja transakcji.

Uruchamianie testu

Po uruchomieniu testu zostanie wyświetlone okno ze szczegółami:

Rys. 17. Wyniki testów.

Warto zwrócić uwagę, że dzięki utworzeniu transakcji możemy badać wydajność serii zapytań, nie wgłębiając się w poszczególne wywołania stron. Powyższe okno pozwala również na obejrzenie wysłanego zapytania (zakładka Request), zwróconej odpowiedzi (zakładka Response), zmiennych kontekstowych (również tych zdefiniowanych samodzielnie za pomocą reguł ekstrakcji) oraz szczegółów.

Rys. 18. Kontekst zawiera również wartości wydobyte za pomocą reguł ekstrakcji.

Wyniki te uwzględniają wyłącznie jedno słowo kluczowe – „ASP.NET” dlatego, że test został uruchomiony jednokrotnie. Jeśli chcemy przetestować stronę dla różnych wartości, należy wybrać Edit Run Setting (okno wyników) oraz ustawić parametr Fixed Run Count na żądaną wartość:

Rys. 19. Test zostanie uruchomiony pięć razy.

Po chwili zostaną przedstawione wyniki dla każdej serii:

Rys. 20. Testy nie zostały zaliczone ze względu na regułę walidacyjną, która wymaga, aby każda strona zawierała tekst „Results”.

Generowanie kodu

Visual Studio pozwala na wygenerowanie kodu źródłowego dla utworzonego testu. W oknie testu wystarczy kliknąć na ikonkę Generate Code, a po chwili pojawi się okienko, w którym należy wpisać nazwę pliku docelowego:

Rys. 21. Nazwa pliku docelowego zawierającego kod źródłowy.

Wygenerowany kod:

[DeploymentItem("..\\testproject\\1.csv", "..\\testproject")]
[DataSource("DataSource1", "Microsoft.VisualStudio.TestTools.DataSource.CSV", "|DataDirectory|\\..\\testproject\\1.csv", Microsoft.VisualStudio.TestTools.WebTesting.DataBindingAccessMethod.Sequential, Microsoft.VisualStudio.TestTools.WebTesting.DataBindingSelectColumns.SelectOnlyBoundColumns, "1#csv")]
    [DataBinding("DataSource1", "1#csv", "Słowa kluczowe", "DataSource1.1#csv.Słowa kluczowe")]
    public class WebTestSourceCode : WebTest
    {

        public WebTestSourceCode()
        {
            this.PreAuthenticate = true;
        }

        public override IEnumerator<WebTestRequest> GetRequestEnumerator()
        {
            // Initialize validation rules that apply to all requests in the WebTest
            if ((this.Context.ValidationLevel >= Microsoft.VisualStudio.TestTools.WebTesting.ValidationLevel.Low))
            {
                ValidateResponseUrl validationRule1 = new ValidateResponseUrl();
                this.ValidateResponse += new EventHandler<ValidationEventArgs>(validationRule1.Validate);
            }
            if ((this.Context.ValidationLevel >= Microsoft.VisualStudio.TestTools.WebTesting.ValidationLevel.Low))
            {
                ValidationRuleResponseTimeGoal validationRule2 = new ValidationRuleResponseTimeGoal();
                validationRule2.Tolerance = 0D;
                this.ValidateResponseOnPageComplete += new EventHandler<ValidationEventArgs>(validationRule2.Validate);
            }
            if ((this.Context.ValidationLevel >= Microsoft.VisualStudio.TestTools.WebTesting.ValidationLevel.High))
            {
                ValidationRuleFindText validationRule3 = new ValidationRuleFindText();
                validationRule3.FindText = "Results";
                validationRule3.IgnoreCase = true;
                validationRule3.UseRegularExpression = false;
                validationRule3.PassIfTextFound = true;
                this.ValidateResponse += new EventHandler<ValidationEventArgs>(validationRule3.Validate);
            }

            this.BeginTransaction("SampleTransaction");

            WebTestRequest request1 = new WebTestRequest("https://www.bing.com/");
            request1.ThinkTime = 1;
            ExtractHiddenFields extractionRule1 = new ExtractHiddenFields();
            extractionRule1.Required = true;
            extractionRule1.HtmlDecode = true;
            extractionRule1.ContextParameterName = "1";
            request1.ExtractValues += new EventHandler<ExtractionEventArgs>(extractionRule1.Extract);
            ExtractHttpHeader extractionRule2 = new ExtractHttpHeader();
            extractionRule2.Header = "Content-Length";
            extractionRule2.Required = true;
            extractionRule2.ContextParameterName = "HTTP_LENGTH";
            request1.ExtractValues += new EventHandler<ExtractionEventArgs>(extractionRule2.Extract);
            yield return request1;
            request1 = null;

            WebTestRequest request2 = new WebTestRequest("http://api.bing.com/qsonhs.aspx");
            yield return request2;
            request2 = null;

            WebTestRequest request3 = new WebTestRequest("https://www.bing.com/HPImageArchive.aspx");
            request3.ThinkTime = 6;
            request3.QueryStringParameters.Add("format", "xml", false, false);
            request3.QueryStringParameters.Add("idx", "0", false, false);
            request3.QueryStringParameters.Add("n", "1", false, false);
            request3.QueryStringParameters.Add("nc", "1287856710995", false, false);
            yield return request3;
            request3 = null;

            WebTestRequest request4 = new WebTestRequest("http://api.bing.com/qsonhs.aspx");
            request4.ThinkTime = 1;
            request4.QueryStringParameters.Add("FORM", "ASAPIH", false, false);
            request4.QueryStringParameters.Add("mkt", "en-WW", false, false);
            request4.QueryStringParameters.Add("type", "cb", false, false);
            request4.QueryStringParameters.Add("cb", "sa_inst.apiCB", false, false);
            request4.QueryStringParameters.Add("q", "a", false, false);
            request4.QueryStringParameters.Add("cp", "1", false, false);
            request4.QueryStringParameters.Add("o", "s+a+p+h", false, false);
            yield return request4;
            request4 = null;

            WebTestRequest request5 = new WebTestRequest("http://api.bing.com/qsonhs.aspx");
            request5.QueryStringParameters.Add("FORM", "ASAPIH", false, false);
            request5.QueryStringParameters.Add("mkt", "en-WW", false, false);
            request5.QueryStringParameters.Add("type", "cb", false, false);
            request5.QueryStringParameters.Add("cb", "sa_inst.apiCB", false, false);
            request5.QueryStringParameters.Add("q", "a", false, false);
            request5.QueryStringParameters.Add("cp", "2", false, false);
            request5.QueryStringParameters.Add("o", "s+a+p+h", false, false);
            yield return request5;
            request5 = null;

            WebTestRequest request6 = new WebTestRequest("http://api.bing.com/qsonhs.aspx");
            request6.QueryStringParameters.Add("FORM", "ASAPIH", false, false);
            request6.QueryStringParameters.Add("mkt", "en-WW", false, false);
            request6.QueryStringParameters.Add("type", "cb", false, false);
            request6.QueryStringParameters.Add("cb", "sa_inst.apiCB", false, false);
            request6.QueryStringParameters.Add("q", "as", false, false);
            request6.QueryStringParameters.Add("cp", "2", false, false);
            request6.QueryStringParameters.Add("o", "s+a+p+h", false, false);
            yield return request6;
            request6 = null;

            WebTestRequest request7 = new WebTestRequest("http://api.bing.com/qsonhs.aspx");
            request7.ThinkTime = 1;
            request7.QueryStringParameters.Add("FORM", "ASAPIH", false, false);
            request7.QueryStringParameters.Add("mkt", "en-WW", false, false);
            request7.QueryStringParameters.Add("type", "cb", false, false);
            request7.QueryStringParameters.Add("cb", "sa_inst.apiCB", false, false);
            request7.QueryStringParameters.Add("q", "asp", false, false);
            request7.QueryStringParameters.Add("cp", "3", false, false);
            request7.QueryStringParameters.Add("o", "s+a+p+h", false, false);
            yield return request7;
            request7 = null;

            WebTestRequest request8 = new WebTestRequest("http://api.bing.com/qsonhs.aspx");
            request8.QueryStringParameters.Add("FORM", "ASAPIH", false, false);
            request8.QueryStringParameters.Add("mkt", "en-WW", false, false);
            request8.QueryStringParameters.Add("type", "cb", false, false);
            request8.QueryStringParameters.Add("cb", "sa_inst.apiCB", false, false);
            request8.QueryStringParameters.Add("q", "asp%20", false, false);
            request8.QueryStringParameters.Add("cp", "4", false, false);
            request8.QueryStringParameters.Add("o", "s+a+p+h", false, false);
            yield return request8;
            request8 = null;

            WebTestRequest request9 = new WebTestRequest("http://api.bing.com/qsonhs.aspx");
            request9.QueryStringParameters.Add("FORM", "ASAPIH", false, false);
            request9.QueryStringParameters.Add("mkt", "en-WW", false, false);
            request9.QueryStringParameters.Add("type", "cb", false, false);
            request9.QueryStringParameters.Add("cb", "sa_inst.apiCB", false, false);
            request9.QueryStringParameters.Add("q", "asp%20.", false, false);
            request9.QueryStringParameters.Add("cp", "5", false, false);
            request9.QueryStringParameters.Add("o", "s+a+p+h", false, false);
            yield return request9;
            request9 = null;

            WebTestRequest request10 = new WebTestRequest("http://api.bing.com/qsonhs.aspx");
            request10.QueryStringParameters.Add("FORM", "ASAPIH", false, false);
            request10.QueryStringParameters.Add("mkt", "en-WW", false, false);
            request10.QueryStringParameters.Add("type", "cb", false, false);
            request10.QueryStringParameters.Add("cb", "sa_inst.apiCB", false, false);
            request10.QueryStringParameters.Add("q", "asp%20.ne", false, false);
            request10.QueryStringParameters.Add("cp", "7", false, false);
            request10.QueryStringParameters.Add("o", "s+a+p+h", false, false);
            yield return request10;
            request10 = null;

            WebTestRequest request11 = new WebTestRequest("http://api.bing.com/qsonhs.aspx");
            request11.ThinkTime = 1;
            request11.QueryStringParameters.Add("FORM", "ASAPIH", false, false);
            request11.QueryStringParameters.Add("mkt", "en-WW", false, false);
            request11.QueryStringParameters.Add("type", "cb", false, false);
            request11.QueryStringParameters.Add("cb", "sa_inst.apiCB", false, false);
            request11.QueryStringParameters.Add("q", "asp%20.net", false, false);
            request11.QueryStringParameters.Add("cp", "8", false, false);
            request11.QueryStringParameters.Add("o", "s+a+p+h", false, false);
            yield return request11;
            request11 = null;

            WebTestRequest request12 = new WebTestRequest("https://www.bing.com/search");
            request12.QueryStringParameters.Add("q", this.Context["DataSource1.1#csv.Słowa kluczowe"].ToString(), false, false);
            request12.QueryStringParameters.Add("form", this.Context["$HIDDEN1.form"].ToString(), false, false);
            request12.QueryStringParameters.Add("qs", "n", false, false);
            request12.QueryStringParameters.Add("sk", "", false, false);
            request12.QueryStringParameters.Add("sc", "8-8", false, false);
            yield return request12;
            request12 = null;

            WebTestRequest request13 = new WebTestRequest("http://api.bing.com/qsonhs.aspx");
            yield return request13;
            request13 = null;

            this.EndTransaction("SampleTransaction");
        }
    }

Wygenerowany kod można modyfikować i nie wpływa to na wcześniejszą konfigurację w IDE. Oba testy są zatem niezależne. Mając do dyspozycji kod c#, łatwo jest tworzyć w pełni zautomatyzowane testy – można m.in. wywoływać kod w pętli lub pobierać dane z usług sieciowych albo z baz danych.

Zakończenie

Testy webowe pozwalają na analizę wydajności aplikacji www. Za pomocą reguł walidacyjnych można również sprawdzić, czy odpowiednie fragmenty strony znajdują się w odpowiedzi po wykonaniu danego zapytania. Utworzone testy webowe można wykorzystać później w testach obciążenia jako jedną ze składowych, obok testów jednostkowych. Dzięki integracji z przeglądarką internetową tester z łatwością może nagrać skomplikowaną interakcję z aplikacją webową.

          

Piotr Zieliński

Absolwent informatyki o specjalizacji inżynieria oprogramowania Uniwersytetu Zielonogórskiego. Posiada szereg certyfikatów z technologii Microsoft (MCP, MCTS, MCPD). W 2011 roku wyróżniony nagrodą MVP w kategorii Visual C#. Aktualnie pracuje w General Electric pisząc oprogramowanie wykorzystywane w monitorowaniu transformatorów . Platformę .NET zna od wersji 1.1 – wcześniej wykorzystywał głównie MFC oraz C++ Builder. Interesuje się wieloma technologiami m.in. ASP.NET MVC, WPF, PRISM, WCF, WCF Data Services, WWF, Azure, Silverlight, WCF RIA Services, XNA, Entity Framework, nHibernate. Oprócz czystych technologii zajmuje się również wzorcami projektowymi, bezpieczeństwem aplikacji webowych i testowaniem oprogramowania od strony programisty. W wolnych chwilach prowadzi blog o .NET i tzw. patterns & practices.