Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nazewnictwo testów - poziomy testowania. #20

Open
dybi opened this issue Sep 26, 2018 · 7 comments
Open

Nazewnictwo testów - poziomy testowania. #20

dybi opened this issue Sep 26, 2018 · 7 comments

Comments

@dybi
Copy link

dybi commented Sep 26, 2018

  1. Testy jednostkowe (Unit Tests)
    Sprawa chyba jasna - testują najmniejszą "jednostkę" kodu: funkcję, metodę, klasę. Kolaboratorów naszej jednostki często mockujemy / patchujemy. Np. testy funkcji walidujących.
class TestFramesListValidation(TestCase):

    def test_that_list_of_ints_is_valid(self):
        try:
            validate_frames([1, 2])
        except Exception:  # pylint: disable=broad-except
            self.fail()

    def test_that_if_frames_is_not_a_list_of_ints_method_should_raise_exception(self):
        with self.assertRaises(FrameNumberValidationError):
            validate_frames({'1': 1})

        with self.assertRaises(FrameNumberValidationError):
            validate_frames((1, 2))
  1. Testy jednostkowe+, testy jenostkowe-integracyjne, testy Django-integracyjne, testy komponentowe (Component Tests)
    Nie wiem jak je dobrze nazwać. Być może wszystkie nazwy są dobre, bo dotyczą trochę różnych przypadków, dla których cechą wspólną jest to, że są ponad zwykłymi testami jednostkowymi, ale nie są to jeszcze typowe testy integracyjne. Nadal używa się w nich mocków i patchy, ale często usługi są faktycznie stawiane (nasłuchują na otwartych portach), zapisują coś-odczytują z bazy danych, strzelają prawdziwymi zapytaniami http, itd. Jako przykład podałbym testy MiddleMana lub SigningService-u.
class TestMiddleManServer:

    @pytest.fixture(autouse=True)
    def setUp(self, unused_tcp_port_factory, event_loop):
        golem_message_frame = GolemMessageFrame(Ping(), 777).serialize(CONCENT_PRIVATE_KEY)
        self.patcher = mock.patch("middleman.middleman_server.crash_logger")
        self.crash_logger_mock = self.patcher.start()
        self.internal_port, self.external_port = unused_tcp_port_factory(), unused_tcp_port_factory()
        self.data_to_send = append_frame_separator(escape_encode_raw_message(golem_message_frame))
        self.timeout = 0.2
        self.short_delay = 0.1
        self.middleman = MiddleMan(internal_port=self.internal_port, external_port=self.external_port, loop=event_loop)
        yield self.internal_port, self.external_port
        self.patcher.stop()

    def test_that_if_keyboard_interrupt_is_raised_application_will_exit_without_errors(self):
        with pytest.raises(SystemExit) as exception_wrapper:
            with mock.patch.object(self.middleman, "_run_forever", side_effect=KeyboardInterrupt):
                self.middleman.run()
        assert_that(exception_wrapper.value.code).is_equal_to(None)
        self.crash_logger_mock.assert_not_called()
  
    def test_that_broken_connection_from_concent_is_reported_to_sentry(self):
        with override_settings(
            CONCENT_PRIVATE_KEY=CONCENT_PRIVATE_KEY,
            CONCENT_PUBLIC_KEY=CONCENT_PUBLIC_KEY,
            SIGNING_SERVICE_PUBLIC_KEY=SIGNING_SERVICE_PUBLIC_KEY,
        ):
            schedule_sigterm(delay=self.timeout)
            fake_client = socket.socket()
            client_thread = get_client_thread(send_data, fake_client,  self.data_to_send, self.internal_port, self.short_delay)
            client_thread.start()

            error_message = "Connection_error"

            with mock.patch(
                "middleman.middleman_server.request_producer",
                new=async_stream_actor_mock(side_effect=Exception(error_message))
            ):
                with pytest.raises(SystemExit):
                    self.middleman.run()
            client_thread.join(self.timeout)
            fake_client.close()
            self.crash_logger_mock.error.assert_called_once()
            assert_that(self.crash_logger_mock.error.mock_calls[0][1][0]).contains(error_message)

lub Djangowe testy dla handlerów:

@override_settings(
    CONCENT_PRIVATE_KEY       = CONCENT_PRIVATE_KEY,
    CONCENT_PUBLIC_KEY        = CONCENT_PUBLIC_KEY,
    CONCENT_MESSAGING_TIME    = 10,  # seconds
    FORCE_ACCEPTANCE_TIME     = 10,  # seconds
    CONCENT_ETHEREUM_PUBLIC_KEY='x' * ETHEREUM_PUBLIC_KEY_LENGTH,
)
class AcceptOrRejectIntegrationTest(ConcentIntegrationTestCase):

    def setUp(self):
        super().setUp()
        self.patcher = mock.patch('core.message_handlers.calculate_subtask_verification_time', return_value=10)
        self.addCleanup(self.patcher.stop)
        self.patcher.start()

    def test_provider_forces_subtask_results_for_task_which_was_already_submitted_concent_should_refuse(self):
        """
        Tests if on provider ForceSubtaskResults message Concent will return ServiceRefused
        if ForceSubtaskResults with same task_id was already submitted.

        Expected message exchange:
        Provider  -> Concent:    ForceSubtaskResults
        Concent   -> Provider:   HTTP 202
        Provider  -> Concent:    ForceSubtaskResults
        Concent   -> Provider:   ServiceRefused
        """

        task_to_compute = self._get_deserialized_task_to_compute(
            timestamp   = "2018-02-05 10:00:00",
            deadline    = "2018-02-05 10:00:15",
        )

        # STEP 1: Provider forces subtask results via Concent.
        # Request is processed correctly.
        serialized_force_subtask_results = self._get_serialized_force_subtask_results(
            timestamp="2018-02-05 10:00:30",
            ack_report_computed_task=self._get_deserialized_ack_report_computed_task(
                timestamp="2018-02-05 10:00:20",
                task_to_compute=task_to_compute,
                signer_private_key=self.REQUESTOR_PRIVATE_KEY,
            )
        )

        with mock.patch(
            'core.message_handlers.payments_service.is_account_status_positive',
            side_effect=_get_provider_account_status_true_mock
        ) as is_account_status_positive_true_mock_function:
            with freeze_time("2018-02-05 10:00:30"):
                response_1 = self.client.post(
                    reverse('core:send'),
                    data                                = serialized_force_subtask_results,
                    content_type                        = 'application/octet-stream',
                )

        is_account_status_positive_true_mock_function.assert_called_with(
            client_eth_address=task_to_compute.requestor_ethereum_address,
            pending_value=task_to_compute.price,
        )

        assert len(response_1.content)  == 0
        assert response_1.status_code   == 202

        self._assert_stored_message_counter_increased(increased_by=3)
        self._test_subtask_state(
            task_id=task_to_compute.task_id,
            subtask_id=task_to_compute.subtask_id,
            subtask_state=Subtask.SubtaskState.FORCING_ACCEPTANCE,
            provider_key=self._get_encoded_provider_public_key(),
            requestor_key=self._get_encoded_requestor_public_key(),
            expected_nested_messages={'task_to_compute', 'report_computed_task', 'ack_report_computed_task'},
            next_deadline=parse_iso_date_to_timestamp("2018-02-05 10:00:45"),
        )
        self._test_last_stored_messages(
            expected_messages=[
                message.TaskToCompute,
                message.ReportComputedTask,
                message.tasks.AckReportComputedTask,
            ],
            task_id=task_to_compute.task_id,
            subtask_id=task_to_compute.subtask_id,
        )
        self._test_undelivered_pending_responses(
            subtask_id=task_to_compute.subtask_id,
            client_public_key=self._get_encoded_requestor_public_key(),
            expected_pending_responses_receive=[
                PendingResponse.ResponseType.ForceSubtaskResults,
            ]
        )

        # STEP 2: Provider again forces subtask results via Concent with message with the same task_id.
        # Request is refused.
        with mock.patch(
            'core.message_handlers.payments_service.is_account_status_positive',
            side_effect=self.is_account_status_positive_true_mock
        ):
            with freeze_time("2018-02-05 10:00:31"):
                response_2 = self.client.post(
                    reverse('core:send'),
                    data                                = serialized_force_subtask_results,
                    content_type                        = 'application/octet-stream',
                )

        self._test_response(
            response_2,
            status       = 200,
            key          = self.PROVIDER_PRIVATE_KEY,
            message_type = message.concents.ServiceRefused,
            fields       = {
                'reason':    message.concents.ServiceRefused.REASON.DuplicateRequest,
                'timestamp': parse_iso_date_to_timestamp("2018-02-05 10:00:31"),
            }
        )
        self._assert_stored_message_counter_not_increased()

        self._assert_client_count_is_equal(2)
  1. Testy integracyjne (Integration Tests)
    Testują współdziałanie/przepływ informacji/dopasowanie interfejsów dla dwóch lub więcej komponentów. Nie używamy w nich już mocków i patchy - uruchamiamy prawdziwe usługi, itp. Niekiedy korzystamy z tzw. "Fake-ów" czyli klientów/klas, itp. z ograniczoną przyciętą funkcjonalnością, za pomocą których "podłączamy" się do testowanych komponentów i inicjujemy przepływ. W testach integracyjnych możemy zaglądać w "bebechy" systemu, tj. wnioskować o tym czy testy przeszły czy nie na podstawie danych normalnie nie dostępnych z zewnątrz.
    Istniejących przykładów jest raczej niewiele, np test z signing_service_integration_test_middleman.

  2. Testy E2E (End-to-End) (End-to-End Tests or E2E Tests)
    Testy przeprowadzane niejako z perspeltywy klienta - testują cały system jako "czarną skrzynkę". Używamy zatem tych samych API/endpointów (punktów wejścia-wyjścia) do sytemu, z których korzystałby klient przy normalnym użytkowaniu. Nie mamy dostęþu do "bebechów" systemu - całe wnioskowanie o tym czy funkcjonalność działa jak należy opiera się o dane z "oficjalnego" wyjścia. Oczywiście, oznacza to, że nie używamy w nich mocków, patchy, itd.
    Przykładem byłyby nasze cluster testy, np. api-e2e-additional-verification-test.

@dybi
Copy link
Author

dybi commented Sep 26, 2018

@Code-Poets/all - można głosować na nazewnictwo punktu 2 ;)

Ja osobiście chyba byłbym za "testy komponentowe"

@PaweuB
Copy link

PaweuB commented Sep 26, 2018

12groszy ode mnie:

Myślę, że warto opatrzyć powyższe nazwy w odpowiedniki angielskojęzyczne, zmniejszajmy wątpliwości do minimum ;)

@rwrzesien
Copy link

można głosować na nazewnictwo punktu 2 ;)

Mi bez różnicy, ale wybierzmy jedną i trzymajmy się jej.

  1. Testy integracyjne
  2. Testy E2E (End-to-End)

Dalej nie do końca jestem przekonany czy to nie jest to samo. Patrząc po opisie wydaje mi się że właśnie te testy typu api-e2e-additional-verification-test bardziej pasują do punktu 3 (bo np. niektóre testują tylko sam Concent, a ten konkretny np. testuje Concent+Conductor+Verifier, ale nie np. SigningService). Więc w sumie wydaje mi się że takich testów jak w pkt 4. nie mamy w tym momencie w ogóle. Ale może muszę się jeszcze nad tym głębiej zastanowić.

Btw. jeżeli jednak te testy rozróżniamy, to może warto byłoby je jakoś zkatalogować? Teraz 3 i 4 siedzą w tym samym miejscu. Można to zrobić na przykład w ramach zadania https://www.pivotaltracker.com/story/show/160512292 . Co sądzicie?

@dybi
Copy link
Author

dybi commented Sep 27, 2018

@rwrzesien, w testach E2E korzystamy z produktu tak, jak korzystałby klient. Tzn. używamy tego samego wejścia i wyjścia (w przypadku Concenta, są to endpointy send/ i receive/) i o zachowaniu systemu wnioskujemy na podstawie tego co od niego otrzymamy - nie zaglądamy w "bebechy".
W przypadku testów integracyjnych tak nie jest - Golem bezpośrednio nie dotyka ani MiddleMana, ani SigninigServiceu.

@PaweuB, ok

@dybi
Copy link
Author

dybi commented Sep 27, 2018

@cameel ? @Jakub89 ? @kbeker ? coś do dodania/odjęcia? ;)

@Jakub89
Copy link

Jakub89 commented Sep 27, 2018

Do dodania nic, ale co do pkt2 ograniczył bym się do jednej nazwy. "testy komponentowe" całkiem oryginalnie brzmią, ale właśnie to może być dla kogoś problemem. "jednostkowe+" też pasują.

@dybi
Copy link
Author

dybi commented Dec 12, 2018

@Code-Poets/all
Dobra, nie dawało mi to spokoju i raz jeszcze zacząłem rozważać.

Otóż po dłuższych rozważaniach doszedłem do wniosku, że w Concencie mamy 5 rodzajów testów na 4/5 poziomach testowania:

  1. Testy jednostkowe (Unit Tests)
  2. Testy komponentowe (Component Tests), np. TestMiddleManServer
  3. Testy integracyjne Django (Django Integration Tests), np. test_integration_subtask_results_verify
  4. Testy integracyjne (Integration Tests), np. signing_service_integration_test_middleman
  5. Testy E2E (E2E Tests), np. api-e2e-additional-verification-test

Jeśli chodzi o "poziom", to różnica między komponentowymi a integracyjnymi Django jest niewielka, ale logicznie są to różne testy - komponentowe testują jakiś wydzielony komponent (np. MiddleMan-a) w izolacji (z różnymi stubami, mockami, itp.), a integracyjne Django mogą zahaczać o kilka różnych komponentów (i mieć różną ilość mocków/patchy vs prawdziwych komponentów). Innymi słowy, zakres (poziomów) testów integracyjnych Django jest dość szeroki - od "prawie" jednostkowych do "regularnych" testów integracyjnych.

Może wydaje się to trochę skoplikowane, ale to najlepszy podział, jaki byłem w stanie wymyśleć.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants