Skip to content

Commit f182492

Browse files
authored
🎉Source Sendgrid: increase unit test coverage at least 90% (#16332)
* Added unit tests * Updated test name * Updated setup * Fix requirements * Updated release stage * Updated expected_records
1 parent 5853d52 commit f182492

File tree

6 files changed

+143
-49
lines changed

6 files changed

+143
-49
lines changed

airbyte-config/init/src/main/resources/seed/source_definitions.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,7 +904,7 @@
904904
documentationUrl: https://docs.airbyte.io/integrations/sources/sendgrid
905905
icon: sendgrid.svg
906906
sourceType: api
907-
releaseStage: alpha
907+
releaseStage: beta
908908
- name: Shopify
909909
sourceDefinitionId: 9da77001-af33-4bcd-be46-6252bf9342b9
910910
dockerRepository: airbyte/source-shopify

airbyte-integrations/connectors/source-sendgrid/README.md

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Sendgrid Source
22

3-
This is the repository for the Sendgrid source connector, written in Python.
3+
This is the repository for the Sendgrid source connector, written in Python.
44
For information about how to use this connector within Airbyte, see [the documentation](https://docs.airbyte.io/integrations/sources/sendgrid).
55

66
## Local development
@@ -21,6 +21,7 @@ development environment of choice. To activate it from the terminal, run:
2121
```
2222
source .venv/bin/activate
2323
pip install -r requirements.txt
24+
pip install '.[tests]'
2425
```
2526
If you are in an IDE, follow your IDE's instructions to activate the virtualenv.
2627

@@ -30,21 +31,22 @@ If this is mumbo jumbo to you, don't worry about it, just put your deps in `setu
3031
should work as you expect.
3132

3233
#### Building via Gradle
33-
From the Airbyte repository root, run:
34+
You can also build the connector in Gradle. This is typically used in CI and not needed for your development workflow.
35+
36+
To build using Gradle, from the Airbyte repository root, run:
3437
```
3538
./gradlew :airbyte-integrations:connectors:source-sendgrid:build
3639
```
3740

3841
#### Create credentials
3942
**If you are a community contributor**, follow the instructions in the [documentation](https://docs.airbyte.io/integrations/sources/sendgrid)
4043
to generate the necessary credentials. Then create a file `secrets/config.json` conforming to the `source_sendgrid/spec.json` file.
41-
Note that the `secrets` directory is gitignored by default, so there is no danger of accidentally checking in sensitive information.
44+
Note that any directory named `secrets` is gitignored across the entire Airbyte repo, so there is no danger of accidentally checking in sensitive information.
4245
See `integration_tests/sample_config.json` for a sample config file.
4346

4447
**If you are an Airbyte core member**, copy the credentials in Lastpass under the secret name `source sendgrid test creds`
4548
and place them into `secrets/config.json`.
4649

47-
4850
### Locally running the connector
4951
```
5052
python main.py spec
@@ -53,12 +55,6 @@ python main.py discover --config secrets/config.json
5355
python main.py read --config secrets/config.json --catalog integration_tests/configured_catalog.json
5456
```
5557

56-
### Unit Tests
57-
To run unit tests locally, from the connector directory run:
58-
```
59-
python -m pytest unit_tests
60-
```
61-
6258
### Locally running the connector docker image
6359

6460
#### Build
@@ -82,22 +78,55 @@ docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-sendgrid:dev check --c
8278
docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-sendgrid:dev discover --config /secrets/config.json
8379
docker run --rm -v $(pwd)/secrets:/secrets -v $(pwd)/integration_tests:/integration_tests airbyte/source-sendgrid:dev read --config /secrets/config.json --catalog /integration_tests/configured_catalog.json
8480
```
81+
## Testing
82+
Make sure to familiarize yourself with [pytest test discovery](https://docs.pytest.org/en/latest/goodpractices.html#test-discovery) to know how your test files and methods should be named.
83+
First install test dependencies into your virtual environment:
84+
```
85+
pip install .[tests]
86+
```
87+
### Unit Tests
88+
To run unit tests locally, from the connector directory run:
89+
```
90+
python -m pytest unit_tests
91+
```
8592

8693
### Integration Tests
87-
1. From the airbyte project root, run `./gradlew :airbyte-integrations:connectors:source-sendgrid:integrationTest` to run the standard integration test suite.
88-
1. To run additional integration tests, place your integration tests in a new directory `integration_tests` and run them with `python -m pytest -s integration_tests`.
89-
Make sure to familiarize yourself with [pytest test discovery](https://docs.pytest.org/en/latest/goodpractices.html#test-discovery) to know how your test files and methods should be named.
94+
There are two types of integration tests: Acceptance Tests (Airbyte's test suite for all source connectors) and custom integration tests (which are specific to this connector).
95+
#### Custom Integration tests
96+
Place custom tests inside `integration_tests/` folder, then, from the connector root, run
97+
```
98+
python -m pytest integration_tests
99+
```
100+
#### Acceptance Tests
101+
Customize `acceptance-test-config.yml` file to configure tests. See [Source Acceptance Tests](https://docs.airbyte.io/connector-development/testing-connectors/source-acceptance-tests-reference) for more information.
102+
If your connector requires to create or destroy resources for use during acceptance tests create fixtures for it and place them inside integration_tests/acceptance.py.
103+
To run your integration tests with acceptance tests, from the connector root, run
104+
```
105+
python -m pytest integration_tests -p integration_tests.acceptance
106+
```
107+
To run your integration tests with docker
108+
109+
### Using gradle to run tests
110+
All commands should be run from airbyte project root.
111+
To run unit tests:
112+
```
113+
./gradlew :airbyte-integrations:connectors:source-sendgrid:unitTest
114+
```
115+
To run acceptance and custom integration tests:
116+
```
117+
./gradlew :airbyte-integrations:connectors:source-sendgrid:integrationTest
118+
```
90119

91120
## Dependency Management
92121
All of your dependencies should go in `setup.py`, NOT `requirements.txt`. The requirements file is only used to connect internal Airbyte dependencies in the monorepo for local development.
122+
We split dependencies between two groups, dependencies that are:
123+
* required for your connector to work need to go to `MAIN_REQUIREMENTS` list.
124+
* required for the testing need to go to `TEST_REQUIREMENTS` list
93125

94126
### Publishing a new version of the connector
95127
You've checked out the repo, implemented a million dollar feature, and you're ready to share your changes with the world. Now what?
96-
1. Make sure your changes are passing unit and integration tests
97-
1. Bump the connector version in `Dockerfile` -- just increment the value of the `LABEL io.airbyte.version` appropriately (we use SemVer).
98-
1. Create a Pull Request
99-
1. Pat yourself on the back for being an awesome contributor
100-
1. Someone from Airbyte will take a look at your PR and iterate with you to merge it into master
101-
102-
### Changelog
103-
See the [docs](https://docs.airbyte.io/integrations/sources/sendgrid#changelog) for the changelog.
128+
1. Make sure your changes are passing unit and integration tests.
129+
1. Bump the connector version in `Dockerfile` -- just increment the value of the `LABEL io.airbyte.version` appropriately (we use [SemVer](https://semver.org/)).
130+
1. Create a Pull Request.
131+
1. Pat yourself on the back for being an awesome contributor.
132+
1. Someone from Airbyte will take a look at your PR and iterate with you to merge it into master.

airbyte-integrations/connectors/source-sendgrid/integration_tests/expected_records.txt

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,3 @@
1-
{"stream": "campaigns", "data": {"created_at": "2021-09-08T09:07:48Z", "id": "3c5a9fa6-1084-11ec-ac32-4228d699bad5", "name": "Untitled Single Send", "status": "triggered", "updated_at": "2021-09-08T09:11:08Z", "is_abtest": false}, "emitted_at": 1631093369000}
2-
{"stream": "campaigns", "data": {"created_at": "2021-09-08T09:04:36Z", "id": "c9f286fb-1083-11ec-ae03-ca0fc7f28419", "name": "Copy of Untitled Single Send", "status": "triggered", "updated_at": "2021-09-08T09:09:08Z", "is_abtest": false}, "emitted_at": 1631093369000}
3-
{"stream": "campaigns", "data": {"created_at": "2021-09-08T08:53:59Z", "id": "4e5be6a3-1082-11ec-8512-9afd40c324e6", "name": "Untitled Single Send", "status": "triggered", "updated_at": "2021-09-08T08:57:08Z", "is_abtest": false}, "emitted_at": 1631093369000}
4-
{"stream": "campaigns", "data": {"created_at": "2021-09-08T08:51:59Z", "id": "06ee105f-1082-11ec-8245-86a627812e3d", "name": "Untitled Single Send", "status": "triggered", "updated_at": "2021-09-08T08:55:08Z", "is_abtest": false}, "emitted_at": 1631093369000}
5-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:13:02Z", "id": "d497b877-6486-11eb-be53-b2a243c7228c", "name": "Campaign 18", "status": "draft", "updated_at": "2021-02-01T12:13:02Z", "is_abtest": false}, "emitted_at": 1631093369000}
6-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:13:00Z", "id": "d36a06e7-6486-11eb-bb4f-823d082c01b8", "name": "Campaign 17", "status": "draft", "updated_at": "2021-02-01T12:13:00Z", "is_abtest": false}, "emitted_at": 1631093369000}
7-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:58Z", "id": "d258b5a0-6486-11eb-8b51-8aa6caa37fdd", "name": "Campaign 16", "status": "draft", "updated_at": "2021-02-01T12:12:58Z", "is_abtest": false}, "emitted_at": 1631093369000}
8-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:56Z", "id": "d12b87ec-6486-11eb-be53-b2a243c7228c", "name": "Campaign 15", "status": "draft", "updated_at": "2021-02-01T12:12:56Z", "is_abtest": false}, "emitted_at": 1631093369000}
9-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:54Z", "id": "d0072250-6486-11eb-bd19-4680e22af4b6", "name": "Campaign 14", "status": "draft", "updated_at": "2021-02-01T12:12:54Z", "is_abtest": false}, "emitted_at": 1631093369000}
10-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:53Z", "id": "cf204596-6486-11eb-bd19-4680e22af4b6", "name": "Campaign 13", "status": "draft", "updated_at": "2021-02-01T12:12:53Z", "is_abtest": false}, "emitted_at": 1631093369000}
11-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:51Z", "id": "cdfe8cde-6486-11eb-be53-b2a243c7228c", "name": "Campaign 12", "status": "draft", "updated_at": "2021-02-01T12:12:51Z", "is_abtest": false}, "emitted_at": 1631093369000}
12-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:49Z", "id": "ccecefcb-6486-11eb-bd77-2a301ccc59da", "name": "Campaign 11", "status": "draft", "updated_at": "2021-02-01T12:12:49Z", "is_abtest": false}, "emitted_at": 1631093369000}
13-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:46Z", "id": "caeabedf-6486-11eb-bd19-4680e22af4b6", "name": "Campaign 10", "status": "draft", "updated_at": "2021-02-01T12:12:46Z", "is_abtest": false}, "emitted_at": 1631093369000}
14-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:43Z", "id": "c93431e7-6486-11eb-8b51-8aa6caa37fdd", "name": "Campaign 9", "status": "draft", "updated_at": "2021-02-01T12:12:43Z", "is_abtest": false}, "emitted_at": 1631093369000}
15-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:41Z", "id": "c7eb9306-6486-11eb-bd19-4680e22af4b6", "name": "Campaign 8", "status": "draft", "updated_at": "2021-02-01T12:12:41Z", "is_abtest": false}, "emitted_at": 1631093369000}
16-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:38Z", "id": "c646b948-6486-11eb-bd77-2a301ccc59da", "name": "Campaign 7", "status": "draft", "updated_at": "2021-02-01T12:12:38Z", "is_abtest": false}, "emitted_at": 1631093369000}
17-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:35Z", "id": "c47ccad9-6486-11eb-8b51-8aa6caa37fdd", "name": "Campaign 6", "status": "draft", "updated_at": "2021-02-01T12:12:35Z", "is_abtest": false}, "emitted_at": 1631093369000}
18-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:33Z", "id": "c36c66f3-6486-11eb-bd19-4680e22af4b6", "name": "Campaign 5", "status": "draft", "updated_at": "2021-02-01T12:12:33Z", "is_abtest": false}, "emitted_at": 1631093369000}
19-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:31Z", "id": "c24fdc68-6486-11eb-bd77-2a301ccc59da", "name": "Campaign 4", "status": "draft", "updated_at": "2021-02-01T12:12:31Z", "is_abtest": false}, "emitted_at": 1631093369000}
20-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:25Z", "id": "be9d147f-6486-11eb-8b51-8aa6caa37fdd", "name": "Third Campaign", "status": "draft", "updated_at": "2021-02-01T12:12:25Z", "is_abtest": false}, "emitted_at": 1631093369000}
21-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:12:18Z", "id": "ba43f256-6486-11eb-bb4f-823d082c01b8", "name": "Second Campaign", "status": "draft", "updated_at": "2021-02-01T12:12:18Z", "is_abtest": false}, "emitted_at": 1631093369000}
22-
{"stream": "campaigns", "data": {"created_at": "2021-02-01T12:10:59Z", "id": "8b17a7b7-6486-11eb-bd77-2a301ccc59da", "name": "First Campaign", "status": "draft", "updated_at": "2021-02-01T12:10:59Z", "is_abtest": false}, "emitted_at": 1631093369000}
231
{"stream": "lists", "data": {"name": "Test List: 19", "id": "0236d6d2-75d2-42c5-962d-603e0deaf8d1", "contact_count": 20, "_metadata": {"self": "https://api.sendgrid.com/v3/marketing/lists/0236d6d2-75d2-42c5-962d-603e0deaf8d1"}}, "emitted_at": 1631093370000}
242
{"stream": "lists", "data": {"name": "List for CI tests, number 30", "id": "041ee031-005e-41e7-ad3b-a427f90f54af", "contact_count": 0, "_metadata": {"self": "https://api.sendgrid.com/v3/marketing/lists/041ee031-005e-41e7-ad3b-a427f90f54af"}}, "emitted_at": 1631093370000}
253
{"stream": "lists", "data": {"name": "my_list", "id": "07315be4-500c-4f30-8217-22cb8e39dd37", "contact_count": 0, "_metadata": {"self": "https://api.sendgrid.com/v3/marketing/lists/07315be4-500c-4f30-8217-22cb8e39dd37"}}, "emitted_at": 1631093370000}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
# This file is autogenerated -- only edit if you know what you are doing. Use setup.py for declaring dependencies.
1+
-e ../../bases/source-acceptance-test
22
-e .

airbyte-integrations/connectors/source-sendgrid/setup.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,27 @@
55

66
from setuptools import find_packages, setup
77

8+
MAIN_REQUIREMENTS = [
9+
"airbyte-cdk",
10+
"backoff",
11+
"requests",
12+
]
13+
14+
TEST_REQUIREMENTS = [
15+
"pytest~=6.1",
16+
"source-acceptance-test",
17+
"requests-mock",
18+
]
19+
820
setup(
921
name="source_sendgrid",
1022
description="Source implementation for Sendgrid.",
1123
author="Airbyte",
1224
author_email="[email protected]",
1325
packages=find_packages(),
14-
install_requires=["airbyte-cdk~=0.1", "backoff", "requests", "pytest==6.1.2", "pytest-mock"],
15-
package_data={"": ["*.json", "schemas/*.json"]},
26+
install_requires=MAIN_REQUIREMENTS,
27+
package_data={"": ["*.json", "schemas/*.json", "schemas/shared/*.json"]},
28+
extras_require={
29+
"tests": TEST_REQUIREMENTS,
30+
},
1631
)

airbyte-integrations/connectors/source-sendgrid/unit_tests/unit_test.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,29 @@
33
#
44

55
import unittest
6-
from unittest.mock import MagicMock
6+
from unittest.mock import MagicMock, patch
77

88
import pendulum
99
import pytest
1010
import requests
1111
from airbyte_cdk.logger import AirbyteLogger
12+
from airbyte_cdk.models import SyncMode
1213
from source_sendgrid.source import SourceSendgrid
13-
from source_sendgrid.streams import Messages, SendgridStream
14+
from source_sendgrid.streams import (
15+
Blocks,
16+
Campaigns,
17+
Contacts,
18+
GlobalSuppressions,
19+
Lists,
20+
Messages,
21+
Segments,
22+
SendgridStream,
23+
SendgridStreamIncrementalMixin,
24+
SendgridStreamOffsetPagination,
25+
SuppressionGroupMembers,
26+
SuppressionGroups,
27+
Templates,
28+
)
1429

1530
FAKE_NOW = pendulum.DateTime(2022, 1, 1, tzinfo=pendulum.timezone("utc"))
1631

@@ -52,3 +67,60 @@ def test_messages_stream_request_params(mock_pendulum_now):
5267
request_params
5368
== "query=last_event_time%20BETWEEN%20TIMESTAMP%20%222019-05-20T13%3A30%3A00Z%22%20AND%20TIMESTAMP%20%222022-01-01T00%3A00%3A00Z%22&limit=1000"
5469
)
70+
71+
72+
def test_streams():
73+
streams = SourceSendgrid().streams(config={"apikey": "wrong.api.key123", "start_time": FAKE_NOW})
74+
75+
assert len(streams) == 15
76+
77+
78+
@patch.multiple(SendgridStreamOffsetPagination, __abstractmethods__=set())
79+
def test_pagination(mocker):
80+
stream = SendgridStreamOffsetPagination()
81+
state = {}
82+
response = requests.Response()
83+
mocker.patch.object(response, "json", return_value={None: 1})
84+
mocker.patch.object(response, "request", return_value=MagicMock())
85+
next_page_token = stream.next_page_token(response)
86+
request_params = stream.request_params(stream_state=state, next_page_token=next_page_token)
87+
assert request_params == {"limit": 50}
88+
89+
90+
@patch.multiple(SendgridStreamIncrementalMixin, __abstractmethods__=set())
91+
def test_stream_state():
92+
stream = SendgridStreamIncrementalMixin(start_time=FAKE_NOW)
93+
state = {}
94+
request_params = stream.request_params(stream_state=state)
95+
assert request_params == {"end_time": pendulum.now().int_timestamp, "start_time": FAKE_NOW}
96+
97+
98+
@pytest.mark.parametrize(
99+
"stream_class, url , expected",
100+
(
101+
[Templates, "https://api.sendgrid.com/v3/templates", []],
102+
[Lists, "https://api.sendgrid.com/v3/marketing/lists", []],
103+
[Campaigns, "https://api.sendgrid.com/v3/marketing/campaigns", []],
104+
[Contacts, "https://api.sendgrid.com/v3/marketing/contacts", []],
105+
[Segments, "https://api.sendgrid.com/v3/marketing/segments", []],
106+
[Blocks, "https://api.sendgrid.com/v3/suppression/blocks", ["name", "id", "contact_count", "_metadata"]],
107+
[SuppressionGroupMembers, "https://api.sendgrid.com/v3/asm/suppressions", ["name", "id", "contact_count", "_metadata"]],
108+
[SuppressionGroups, "https://api.sendgrid.com/v3/asm/groups", ["name", "id", "contact_count", "_metadata"]],
109+
[GlobalSuppressions, "https://api.sendgrid.com/v3/suppression/unsubscribes", ["name", "id", "contact_count", "_metadata"]],
110+
),
111+
)
112+
def test_read_records(
113+
stream_class,
114+
url,
115+
expected,
116+
requests_mock,
117+
):
118+
try:
119+
stream = stream_class(start_time=FAKE_NOW)
120+
except TypeError:
121+
stream = stream_class()
122+
requests_mock.get("https://api.sendgrid.com/v3/marketing", json={})
123+
requests_mock.get(url, json={"name": "test", "id": "id", "contact_count": 20, "_metadata": {"self": "self"}})
124+
records = list(stream.read_records(sync_mode=SyncMode))
125+
126+
assert records == expected

0 commit comments

Comments
 (0)