The importance of tests and next steps
Agenda
- recap why we write tests & journey
- facing the problem
- testing in general & best practices
- useful patterns
- coding
The usefulness of testing
- you implement a new feature
- you manually test it locally, on staging, etc.
- other dev needs to change it weeks later
- other dev forgets to test the same previous use cases
- the feature gets broken
Outputs
1. software product for the end users
2. code (for ourselves)
So we have to care about the maintainability of our codebase
Question is that:
How we can write maintainable code?
Automated testing helps
to increase the maintainability of the entire codebase!
But how?
Tests
1. document the expected behaviour (use cases)
2. double-check during coding (less bug)
3. drive design into right direction (testability)
4. continuous feedback during development
(essential for refactoring)
What are the attributes of good test cases?
Tests
- are isolated (don't affect each other)
- are readable and understandable (structure & output)
- document the expected behaviour (BDD)
- don't depend on implementation
- easily executable and repeatable (it's essential)
Best practices
from our codebase
Naming and structure
class <EntityName>TestCase(TestCase):
def test_<method_name>_<behaviour>_<condition>(self):
# expected data & setup data
# action
# expectationsUsing factories
class PlatformMessageTemplateFactory(factory.django.DjangoModelFactory):
class Meta:
model = PlatformMessageTemplate
key = factory.Faker("slug")
miro_url = factory.Faker("url")
value = factory.Faker("text")
Outgoing requests
class Five9ApiServiceTestCase(SimpleTestCase):
@responses.activate
def test_add_to_list_requests_contact_addition(self):
expected_response = {"success": True}
expected_query_parameters = {
**self.expected_params,
"F9domain": settings.FIVE9_API_DOMAIN,
"F9list": "demo.list",
"F9CallASAP": 1,
}
responses.get(
f"{settings.FIVE9_API_BASE_URL}/web2campaign/AddToList",
match=[responses.matchers.query_param_matcher(expected_query_parameters)],
json=expected_response,
)
response = Five9ApiService.add_to_list(self.valid_payload)
self.assertEqual(response.json(), expected_response)
self.assertEqual(response.status_code, HTTP_200_OK)Testing list of entities
class GuidebookPageListViewTestCase(APITestCase):
def test_responds_with_ordered_guidebook_pages(self):
self.guidebook_page.display_order = 1
self.guidebook_page.save()
expected_guidebook_pages: list[GuidebookPage] = [
GuidebookPageFactory.create(
display_order=0,
publish=True,
),
self.guidebook_page,
]
response = self._fetch_guidebook_pages(self.mentor)
self.assertEqual(len(response.data), len(expected_guidebook_pages))
for index, guidebook_page in enumerate(expected_guidebook_pages):
self.assertEqual(response.data[index]["id"], guidebook_page.id)Reusable data
class DispositionWebhookAPITestCase(APITestCase):
def test_returns_unauthorized_for_invalid_token(self):
response = self._request_disposition_webhook({**self._valid_payload, "api_key": "wrong"})
self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED)
@property
def _valid_payload(self) -> dict:
return {
"api_key": self.api_key_configuration.value,
"disposition_id": f"{DispositionEventChoices.INTERVIEW_SCHEDULED}",
"disposition_name": "This was a successful call!",
}
def _request_disposition_webhook(self, payload):
return self.client.post("/integrations/five9/disposition/", payload, format="json")Clean state
class DispositionWebhookAPITestCase(APITestCase):
def setUp(self):
self._clear_configuration()
self.api_key_configuration: Configuration = ConfigurationFactory.create(
key=ConfigurationOptions.F9_API_KEY,
value="secret",
)
def tearDown(self):
self._clear_configuration()
Time related tests
CURRENT_DATETIME = timezone.now().replace(microsecond=0)
class StudentFinalSurveyUpdateViewTestCase(APITestCase):
def test_final_survey_submitted_at_set_to_current_time(self):
with time_machine.travel(CURRENT_DATETIME):
self._update_student_final_survey(self.student_final_survey.uuid, self._get_valid_payload(), self.student)
self.student_final_survey.refresh_from_db()
self.assertEqual(self.student_final_survey.submitted_at.replace(microsecond=0), CURRENT_DATETIME)
Unit tests on frontend
import { profileTimezoneOffset } from 'src/utils/profileTimezoneOffset';
describe('profileTimezoneOffset', () => {
it('should match the following values for Budapest', () => {
expect([-120, -60]).toContain(profileTimezoneOffset('Europe/Budapest'));
});
it('should match the following values for New York', () => {
expect([240, 300]).toContain(profileTimezoneOffset('America/New_York'));
});
it('should match the following values for Vancouver', () => {
expect([420, 480]).toContain(profileTimezoneOffset('America/Vancouver'));
});
it('should match the following values for Tokyo', () => {
expect(profileTimezoneOffset('Asia/Tokyo')).toStrictEqual(-540);
});
});
Gherkin E2E tests
Feature: Counselor Application
Background: User is on counselor application page
Given I am on the counselor application page
Scenario: App stores input even after reload
When I fill an input labeled 'Phone number:'
And page is reloaded
Then input labeled 'Phone number:' has correct value
thanks to Mate!
Test-driven development
1. write a failing test case for the expected behaviour
2. fix the failing test case with implementation
3. refactor
It might seem easy, but it takes a lot o practise!
Conclusion
Tests are also living part of the codebase!
Treat them as production code!
Write good quality production code,
and even better quality tests!
Thanks!
Copy of Internal Presentation #1: SPAs and modern front-end architecture
By Róbert Beretka
Copy of Internal Presentation #1: SPAs and modern front-end architecture
Presentation for Email Channel Crew about NodeJS project setup with TypeScript and best practices.
- 152