Testy jednostkowe a modyfikacja metod.

0

Witam, piszę testy jednostkowe do istniejącego już kodu.

Więc robię to tak:

Mam metodę, która mam przetestować niech to będzie:

public JsonReuslt GetOrdersForCustomersList(DataSourceRequest request); 

Zwraca ona listę w formacie JSON.
Jednakże w ciele metody są odwołania do zew. dostawców oraz modyfikacja uzyskanych danych tak więc mockuje owe providery i sprawiam by metoda przeszła na moich
wymyślonych danych dla różnych scenariuszy(ify, switche, pusty rezultat, nie pusty rezultat)

Dobrze myślę?

No i po commicie ktoś naprawia jakiegoś buga i modyfikuje ciało tej metody GetOrdersForCustomersList
np. zmienia providera, zmienia operacje na danych albo typ rezultatu zamiast

 return Json(result);
// na
return Json(result.ToDataSourceResult(request, x => {
 operacje;
return x;
}));

No i po takiej czy takiej zmianie owy test już nie przechodzi ponieważ sprawdzany był np.
typ rezultatu, wartość jakieś zmiennej lub nie ma takiej nazwy metody(wew.) lub coś wew. innego się zmieniło co wywala test.

I teraz pojawia się moje pytanie.
Czy moje testy są złe czy implementacja została zmieniona w taki sposób, że test trzeba zmienić?

Pytałem przełożonego to mówił, że moje testy są złe ponieważ testują wnętrze testowanej metody i Assertują jej wynik a nie samą metodę(nie wiem co to miało znaczyć, chyba sam wynik?).
Chyba czegoś nie rozumiem bo wydaje mi się gdy metoda testowana odwołuje się do jakiś zew. danych, providerów, serwisów to nie powinno nas to interesować tylko wstawić tam przykładowe dane i mockować co trzeba.

Mógłby ktoś wytłumaczyć mi?

0

Moim skromnym zdaniem, jesli jakas metoda w swoim ciele odwoluje sie do zewnetrznych serwisow, to te zewnetrzne serwisy powinny byc wstrzykiwane do klasy w ktorej ta metoda siedzi (przez konstrukor/property/whatever) wlasnie po to, zeby rozdzielic zaleznosc. Druga sprawa, unit testy nie powinny ingerowac w cialo metody, czyli w jej implementacje. Unit testy powinny byc proste, mockujesz te potrzebne serwisy, odpalasz metode, sprawdzasz wynik i tyle, nie zmieniasz zadnej implementacji

0

No te zew. serwisy są wstrzykiwane poprzez konstruktor.
No ale większość z nich odwołuje się do bazy danych lub innych rzeczy, których bez postawienia aplikacji nie będzie więc trzeba ja zamockować.

  1. Mockuje
  2. Tworze obiekt w którym jest metoda testowana i wstrzykuje zależności poprzez konstruktor
  3. Ustawiam dane zwrócone z zew. serwisów tak by przeszły jakiś warunek
  4. Sprawdzam czy dane zwrócone z metody pokrywają się z oczekiwanym wynikiem
0

No to teoretycznie powinno byc ok

0

Mając taką metodę:

 
 public JsonResult Delete(int id)
       {
            return Json(new { success = orderProvider.DeleteProductionOrder(id) });
        }

Chcę przetestować scenariusz by success był true

więc:

  
 public void Delete_PositiveIdNumber_ReturnsJsonWithTrueSuccess()
        {
            this._orderProvider.DeleteProductionOrder(Arg.Is<int>(i => i > 0)).Returns(true); // orderProvider jest mockiem wstrzykiwanym do controller poprzez konstruktor

            var jsonResult = this._controller.Delete(1);

            Assert.NotNull(jsonResult);
            Assert.Equal("{ success = True }", jsonResult.Data.ToString());
        }
0

@Zimny Szczur to jest trochę bardziej skomplikowana kwestia i szef ma poniekąd rację, ale trudno wyrokować bo nie widzimy konkretnego kodu.
Idea unit testów jest taka żeby jednostkowo testować metodę, ale ze względu na jej DZIAŁANIE a nie STRUKTURĘ. Wielu ludzi popełnia ten błąd i robi testy które sprawdzają nie czy metoda działa poprawnie tylko "czy ciało metody jest takie samo jak kiedy pisaliśmy test".

Co może przykładowo wskazywać na to że "przesadziliśmy":

  • potrzeba mockowania prywatnych metod
  • potrzeba ustawiania stanu prywatnych pól / czytania stanu prywatnych pól
  • potrzeba robienia częściowych mocków (tzn mockujemy część metod testowanej właśnie klasy)
  • zmiana w kodzie polegająca np. na zamianie instrukcji miejscami powoduje wywalenie testu mimo że z punktu widzenia logiki taka zmiana jest uprawniona

Niemniej oczywiście bywa tak że metoda się zwyczajnie zmieniła i trzeba poprawić test, ale to zwykle "od razu widać" bo np. argumenty metody się nie zgadzają, albo próbujemy wstrzykiwać serwis którego metoda już nie używa, bo używa innego.

0

Dzięki za odpowiedź. Nie mogę chyba zbyt dużo kodu wrzucać do internetu.

Mógłbyś opisać dokładniej o co chodzi z testowaniem działania a struktury?

Co do ustawiania pól prywatnych liczą się też pola wew. metody?

Chyba nie rozumiem do końca idei testów. I testuje jej wew. działanie.

0
Shalom napisał(a):

Idea unit testów jest taka żeby jednostkowo testować metodę, ale ze względu na jej DZIAŁANIE a nie STRUKTURĘ. Wielu ludzi popełnia ten błąd i robi testy które sprawdzają nie czy metoda działa poprawnie tylko "czy ciało metody jest takie samo jak kiedy pisaliśmy test".

Na miesiąc ich "do" TDD i się oduczą takiego sposobu myślenia :)

0

To może dam większy przykład:
Zmienne i konstruktor klasy pod testem.

private IPayersProvider payersProvider;

        public PayersController(IPayersProvider payersProvider)
        {
            this.payersProvider = payersProvider;
        }
 

Metoda do testu.

 
      public ActionResult EditDriver(int id)
        {

            var driverEntity = this.payersProvider.GetDriverById(id);
            if (driverEntity == null)
            {
                throw new ArgumentNullException(string.Format("Driver with id={0} was not found", id));
            }

            var model = new EditDriverModel
            {
                Id = driverEntity.Id,
                TransportCompanyId = driverEntity.TransportCompanyId,
                CarId = driverEntity.CarId,
                FirstName = driverEntity.FirstName,
                LastName = driverEntity.LastName,
                Phone = driverEntity.Phone,
                TransportCompanyName = driverEntity.TransportCompany.Company.Name
            };
            var cars = this.payersProvider.GetAllCars().Select(x => new { x.Id, x.Name }).ToList();
            model.Cars = new SelectList(cars, "Id", "Name", model.CarId);
            return View(model);
        }

Zmienne i konstruktor klasy testowej:

private readonly PayersController _controller;
        private readonly IPayersProvider _provider;

        // Ctor
        public PayersTests()
        {
            this._provider = Substitute.For<IPayersProvider>();
            this._controller = new PayersController(this._provider);
        }
 

Metoda testu:

[Fact]
        public void EditDriver_PositiveIdNumber_ReturnsViewWithEmptyNameAndWithFilledModel()
        {
            // arrange
            this._provider.GetDriverById(Arg.Is<int>(i => i > 0)).Returns(
                new Driver()
                {
                    Id = 1,
                    TransportCompanyId = 1,
                    CarId = 1,
                    FirstName = "",
                    LastName = "",
                    Phone = "",
                    TransportCompany = new TransportCompany()
                    {
                        Company = new DatabaseModel.Model.Common.Company()
                        {
                            Name = ""
                        }
                    }
                });

            this._provider.GetAllCars().Returns(
                new List<Car>()
                {
                    new Car()
                    {
                        Id = 1,
                        Name = ""
                    }
                }.AsQueryable());

            // act
            var actionResult = this._controller.EditDriver(1) as ViewResult;
            var edm = (EditDriverModel)actionResult.Model;

            // assert
            Assert.NotNull(actionResult);
            Assert.Equal("", actionResult.ViewName);
            Assert.NotEmpty(edm.Cars);

        }

Czy to jest poprawny test?

0

Nie wiem co to jest pole wew. metody ale też nie programuje w .net więc to może dlatego.
Przykład będzie w javie, ale postaram się żeby był dość jasny. Mamy sobie klasę:

class SumOddNumbers{

  @Inject
  private ParityChecker parityChecker;
  @Inject
  private Summator summator;

  public int sum(List<Integer> input){
      summator.clear();
      Iterator<Integer> it = input.iterator();
      while(it.hasNext()){
          Integer number = it.next();
          if (parityChecker.isOdd(number)){
              summator.add(number);
          }
      }
      return summator.getSum();
  }
}

I chcemy ten kod przetestować nie zaprzęgając do tego ani Summatora ani ParityCheckera. est mógłby wyglądać na przykład tak:

@Test
public void testAddOddNumbers(){
    SumOddNumbers sumOddNumbers = new SumOddNumbers();
    List<Integer> input = Arrays.asList(1,2,3,4);
    expect(parityChecker.isOdd(anyInt()).andReturn(true).anyTimes();

    summator.clear();
    expectLastCall().atLeastOnce();
    for(int element : input){
        summator.add(element);
        expectLastCall().once();
    }
    int expectedResult = 10;
    expect(summator.getSum()).andReturn(expectedResult).atLeastOnce();
    
    replayAll();
    int result = sumOddNumbers.sum(input);
    verifyAll();
    assertEquals(expectedResult, result);
}

Test jest ustawiony tak, że uzna każdą liczbę za nieparzystą. Nasze oczekiwania wobec testu są takie, że sumator zostanie wyzerowany, następnie każdy element wejścia zostanie dodany raz, a wynikiem działania metody będzie suma zwrócona przez sumator.
Jak mógłby wyglądać źle napisany test? Na przykład tak:

@Test
public void testAddOddNumbers(){
    SumOddNumbers sumOddNumbers = new SumOddNumbers();
    List<Integer> input = createMock(List.class);
    Iterator<Integer> iterator = createMock(Iterator.class);
    expect(input.iterator()).andReturn(iterator).once(); // oczekujemy że ktoś zawoła iterator i weźmie go raz, bo tak jest w kodzie
    int elementsInInput = 4;
    expect(iterator.hasNext()).andReturn(true).times(elementsInInput); // oczekujemy że ktoś raz przeleci iteratorem całe wejście
    expect(iterator.next()).andReturn(0).times(elementsInInput);
    expect(parityChecker.isOdd(anyInt()).andReturn(true).times(elementsInInput); // oczekujemy że parzystość dla każdej liczby zostanie sprawdzona tylko raz, bo tak jest w aktualnej implementacji

    summator.clear();
    expectLastCall().once(); // oczekujemy że sumator zerowany będzie tylko raz, bo tak jest w aktualnej implementacji
    for(int element : input){
        summator.add(element);
        expectLastCall().once();
    }
    int expectedResult = 10;
    expect(summator.getSum()).andReturn(expectedResult).once();
    
    replayAll();
    int result = sumOddNumbers.sum(input);
    verifyAll();
    assertEquals(expectedResult, result);
}

Widzisz co z tym testem jest nie tak? Co się stanie jeśli zamienie sobie tą pętlę w kodzie np. na pętlę

for(int i=0;i<input.size();i++){
    Integer number = input.get(i);
    //itd
}

? Test sie wysypie, mimo że kod działa poprawnie.

Albo co będzie jeśli dodam sobie w kodzie logowanie i będzie np.

          if (parityChecker.isOdd(number)){
              logger.log("Parity check = "+parityChecker.isOdd(number));
              summator.add(number);
          }

Znów test się sypnie ;]

Edit: moim zdaniem ten "test" który pokazałeś niczego nie testuje. Masz tam 3 asercje z czego dwie pierwsze są bez sensu bo nijak nie testują twojego kodu a trzecia sprawdza za mało żeby mieć jakiekolwiek znaczenie. Przecież ja mógłbym teraz wziąć tą twoją metodę, wywalić całe jej ciało, utworzyć pusty EditDriverModel, przypisać mu do cars ala ma kota a sierotka ma rysia a twój test nadal by twierdził że wszystko jest ok :D

Edit2:
Ja tu widze miejsce na przynajmniej kilka osobnych testów. Jeden test na ten wyjątek, tzn mockujesz rzucenie wyjątku i sprawdzasz czy metoda zachowa sie tak jak powinna. Poza tym ten kawałek z tworzeniem EditDriverModel można wydzielić do osobnej metody i napisać test który sprawdzi czy poprawnie przepisujesz dane z miejsce na miejsce. Test tej głównej metody jak dla mnie powinien zawierać:

  • weryfikacje czy pobraliśmy z bazy odpowiedniego drivera
  • weryfikacje czy do utworzenia EditDriverModel przekazaliśmy ten pobrany obiekt
    Dodatkowo możemy zrobić:
  • weryfikacje czy pobraliśmy z bazy listę samochodów
  • weryfikacje czy utworzyliśmy selection list z listy samochodów
  • weryfikacje czy lista samochodów została zwrócona w modelu
0

Jak niby sprawdzić te rzeczy o których napisałeś?

Jak niby weryfikować te pobrania z bazy?
Mogę ją jedynie zmockować.

Czyli test do bani?
Napisałem takich prawie 300 T_T

Możecie polecić jakiś artykuł, tutorial do takich testów? Wszystko co znalazłem to sprawdza jakieś proste metody Add, Divide a żadne z mockami, bazą danych a jak już to jakieś tandetne przykłady.

1

Oczywiście że jest do bani bo:

  1. Mogę popsuć metodę na wszystkie możliwe sposoby a test będzie nadal przechodził. Wystarczy że przypisze cos niepustego do model.cars i test zawsze przejdzie niezależnie od tego co ta metoda zrobi. Gdzie tu sens? Co wg ciebie ten test w takim razie weryfikuje? o_O
  2. Jest jeszcze lepiej, bo mogę też wysypać test mimo, że metoda będzie poprawna! Wystarczy że GetAllCars zwróci pustą kolekcje! Jak przypuszczam jest to sytuacja zupełnie poprawna z punktu widzenia logiki aplikacji, a jednak test zasygnalizuje ją jako błąd.

Jeśli chodzi o te weryfikacje to jasne że chodzi o mocka. Ale chodzi o to żeby upewnić sie że pobieramy poprawnego drivera! Bo mogę sobie zmienic w metodzie przypadkiem
var driverEntity = this.payersProvider.GetDriverById(id); na var driverEntity = this.payersProvider.GetDriverById(69); bo akurat cośtam sobie testowałem i sztywno ustawiłem id w kodzie a potem zapomniałem i chce commitować. Test powinien się wysypać bo powinien weryfikować czy pobieramy to co powinniśmy. Analogicznie z tym EditDriverModel, powinieneś sprawdzić czy faktycznie poprawnie tworzymy obiekt na bazie tego co przyszło nam z mockowanej bazy. Bo ktoś może w czasie refaktoringu coś zahardkodować albo przypadkiem zamienić jeden parametr z drugim (np. przypisać firstName do lastName) i test powinien to wyłapać.

0

Czyli sprawdzać czy Id wyjściowego obiektu jest takie samo jak wejściowe oraz sprawdzać czy typ obiektu jest taki jak oczekiwany oraz wszystkie wew. zmienne i kolekcje czy są odpowiedniego typu oraz logicznie ustawione?

No ale jak mam 2 testy i jeden sprawdza scenariusz czy kolekcja zwrócona jest pusta druga czy nie to złączyć to w jeden czy co?

0

Tak po prawdzie to ja bym to podzielił na 4 osobne metody.

  1. Pobranie drivera z bazy / rzucenie wyjątku
  2. Mapowanie drivera na EditDriverModel
  3. Tworznie SelectList
  4. Główna metoda która woła te 3 pozostałe

Do 1) zrobiłbym dwa testy, które weryfikują że do serwisu payersProvider przekazujesz poprawne ID drivera, a dodatkowo jeden test z rzuceniem wyjątku.
Do 2) zrobiłbym prosty test który asercjami sprawdza czy poprawnie przypisano wszystkie pola
Do 3) zrobiłbym prosty test który asercją sprawdza czy utworzona została lista z tego co mock zwrócił jako dane z bazy
Do 4) zrobiłbym test który weryfikuje czy poprawnie zmontowałeś model na bazie tych 3 metod.

0

Świetna odpowiedź!

Czyli sprawdzać typy, wartości i czy się zgadza a nie tylko czy coś nie jest nullem czy nie jest puste. Proste a takie trudne :)

Podział tej dużej metody to na prywatne i testować tą główną w różne przypadki czy publiczne?

Do każdego scenariusza mam osobny test tak więc do przypadku wyjątku:

[Fact]
        public void EditDriver_NegativIdNumber_ThrowsArgumentNullException()
        {
            this._provider.GetDriverById(Arg.Is<int>(i => i < 0)).Returns(x => null);

            Assert.Throws<ArgumentNullException>(
                () => this._controller.EditDriver(-1));
        }

Mówisz, że byś sprawdził czy metoda providera została wywołana z odpowiednimi argumentami czyli robić osobny test dla sprawdzania tylko czy metoda została wywołana a jeśli ktoś zmieni kod metody tak by odbierały inne parametry lub inaczej to kicha tak?

Po tych odpowiedziach napisałem coś takiego:

Kod klasy testowanej.

public class ClientPanelController : BaseController
    {
        private IOrderProvider orderProvider;
        private IClientsProvider clientsProvider;
        private IUserProvider usersProvider;

        public ClientPanelController(IOrderProvider orderProvider, IClientsProvider clientsProvider, IUserProvider userProvider)
        {
            this.orderProvider = orderProvider;
            this.clientsProvider = clientsProvider;
            this.usersProvider = userProvider;
        }

        // GET: OrderManagement
        public async Task<ActionResult> Index()
        {
            if (!Request.IsAuthenticated)
            {
                throw new Exception(string.Format("User not authenticated"));
            }

            var user = this.usersProvider.GetUserByName(User.Identity.Name);
            if (user == null)
            {
                throw new ArgumentNullException(string.Format("User with name={0} was not found", User.Identity.Name));
            }

            if (!user.CompanyId.HasValue)
            {
                return View(new ClientPanelModel());
            }

            var client = await this.clientsProvider.GetClientByComapanyIdAsync(user.CompanyId.Value);
            if (client == null)
            {
                return View(new ClientPanelModel());
            }

            return View(new ClientPanelModel
                {
                    IsMatched = true
                });
        }
    }
 

Napisałem do tego takie testy:

public class ClientPanelTests : IDisposable
    {
        // variables
        readonly IOrderProvider orderProvider;
        readonly IClientsProvider clientsProvider;
        readonly IUserProvider usersProvider;

        readonly ClientPanelController _controller;

         // ctor
        public ClientPanelTests()
        {
            this.orderProvider = Substitute.For<IOrderProvider>();
            this.clientsProvider = Substitute.For<IClientsProvider>();
            this.usersProvider = Substitute.For<IUserProvider>();

            this._controller = new ClientPanelController(
                this.orderProvider, this.clientsProvider, this.usersProvider);
        }
        // methods
        public void Dispose()
        {
            this._controller.Dispose();
        }
        private void SetUserIsAuthenticatedAndUserIdentityName(bool isAuthenticated, string userName = null)
        {
            var httpRequestMock = new Mock<HttpRequestBase>();
            var httpContextMock = new Mock<HttpContextBase>();
            httpRequestMock.Setup(m => m.IsAuthenticated).Returns(isAuthenticated);
            httpContextMock.Setup(m => m.Request).Returns(httpRequestMock.Object);

            if (userName != null)
                httpContextMock.Setup(m => m.User.Identity.Name).Returns(userName);

            var controllerContext = new ControllerContext(
                new System.Web.Routing.RequestContext(
                    httpContextMock.Object, new System.Web.Routing.RouteData()),
                    this._controller);
            this._controller.ControllerContext = controllerContext;
        }
            // tests
        [Fact]
        public void Index_IsNotAuthenticated_ThrowsException()
        {
            // arrange
            SetUserIsAuthenticatedAndUserIdentityName(false);


            var expectedMsg = "User not authenticated";

            // act & assert
            var exceptionMsg = Assert.ThrowsAsync<Exception>(
                () => this._controller.Index()).Result.Message;

            Assert.Equal(expectedMsg, exceptionMsg);
        }

       [Fact]
        public void Index_SampleUser_GetUserByNameWasCalled()
        {
           // arrange
            string userName = "Test user";
            SetUserIsAuthenticatedAndUserIdentityName(true, userName);

           // act 
            this._controller.Index();

           // assert
            this.usersProvider.Received(1).GetUserByName(userName);
        }
        [Fact]
        public void Index_GetUserByNameReturnsNull_ThrowsArgumentNullException()
        {
            // arrange
            SetUserIsAuthenticatedAndUserIdentityName(true, "");


            DatabaseModel.ApplicationUser user = null;
            this.usersProvider.GetUserByName(Arg.Is<string>(s => s == ""))
                .Returns(user);

            var expectedMsg = string.Format("User with name={0} was not found",
                this._controller.User.Identity.Name);

            // act & assert
            var exceptionMsg = Assert.ThrowsAsync<ArgumentNullException>(
                () => this._controller.Index()).Result.ParamName;

            Assert.Equal(expectedMsg, exceptionMsg);
        }
        [Fact]
        public void Index_UserCompanyIdIsWihtoutValue_ReturnsViewWithEmptyNameAndClientPanelModel()
        {
            // arrange
            SetUserIsAuthenticatedAndUserIdentityName(true, "Test user");


            DatabaseModel.ApplicationUser user = new DatabaseModel.ApplicationUser
            {
                CompanyId = null
            };
            this.usersProvider.GetUserByName(Arg.Is<string>(s => s.Length > 0))
                .Returns(user);

            // act
            var actionResult = this._controller.Index().Result as ViewResult;

            // assert
            Assert.NotNull(actionResult);
            Assert.Equal("", actionResult.ViewName);
            Assert.IsType(typeof(ClientPanelModel), actionResult.Model);
        }
        [Fact]
        public void Index_GetClientByComapanyIdReturnSampleClient_ReturnsViewWithEmptyNameAndClientPanelModelWithIsMatched()
        {
            // arrange
            SetUserIsAuthenticatedAndUserIdentityName(true, "Test user");


            DatabaseModel.ApplicationUser user = new DatabaseModel.ApplicationUser
            {
                CompanyId = 1
            };
            this.usersProvider.GetUserByName(Arg.Is<string>(s => s.Length > 0))
                .Returns(user);

            this.clientsProvider.GetClientByComapanyIdAsync(Arg.Is<int>(i => i > 0))
                .Returns(
                Task.Run(() => new Client()));

            // act
            var actionResult = this._controller.Index().Result as ViewResult;
            var data = actionResult.Model as ClientPanelModel;

            // assert
            Assert.NotNull(actionResult);
            Assert.Equal("", actionResult.ViewName);
            Assert.IsType(typeof(ClientPanelModel), data);
            Assert.True(data.IsMatched);
        }
    }
 

Trochę długie ale starałem się opisać każdy scenariusz.
Czy o to chodziło? O takie sprawdzanie?

0

Nie, nie do końca.

  1. Za dużo upchane do kontrolera. Kontroler powinien zawierać zero logiki! Kontroler tylko woła inne serwisy i manipuluje widokiem! Zasadniczo pisanie testów dla dobrego kontrolera nie ma sensu bo w kontrolerze jest tylko:
x = serwis.wywołaj();
return widok("widok.html", x);

I w takim kodzie w zasadzie nie ma czego testować, najwyżej sprawdzić czy serwis jest wołany i czy zwracany jest odpowiedni widok.

  1. Załóżmy na chwilę że ten twój kod jest jednak w osobnym serwisie. Nadal uważam że należy do podzielić na metody...
  2. Asercje na stringach z komunikatem błędu to głupi pomysł. Asercja powinna sprawdzć tylko czy rzucony został wyjątek odpowiedniego typu czy nie.
  3. Nie powinieneś generalnie testować tylko "kawałka" metody. Jeśli musisz to znaczy że metoda jest za duża, trzeba ją podzielić i testować kawałki osobno.
0

No wiem, że są za duże te kontrolery, czytałem wątki na forum i nie powinno być takiego syfu ale to nie mój kod ja jestem tylko od testów.

Hmm co do sprawdzania wiadomości wyjątku to co w przypadku gdy jedna metoda rzuca kilka tych samych wyjątków ale z innym komunikatem?

Dzięki za odpowiedzi wiele mi dały :)

0

To znów znaczy że metoda jest za duża i zbyt skomplikowana ;] Niemniej zauważ że ciebie interesuje w teście tylko to że:

  • dla poprawnego wejścia metoda działa ok
  • dla niepoprawnego rzuca wyjątek
    Jeśli może rzucic wyjątek dla różnych rodzajów niepoprwnych wejść (np. dla jednego lub drugiego parametru) to nic to nie zmienia. Ot piszesz dwa testy które sprawdzą obie sytuacje.
0

Dzięki wielkie ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1