Skip to content

Nazewnictwo testów - poziomy testowania. #20

@dybi

Description

@dybi
  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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions