Давненько вынашиваю идею поиграться с последней версией .net framework’а и Docker’ом. И так удачно совпало, что есть у меня задача при решении которой я получу практическую пользу. Речь идёт о передаче показаний счётчиков воды из облака Saures в Томский расчётный центр. Немного контекста: Saures — это Российская компания занимающаяся автоматизацией учёта и контроля коммунальных ресурсов. Каталог оборудования компании позволяет организовать очень интересные сценарии. В простейшем случае для учёта потребления воды понадобятся счётчики и контроллер. Контроллер передаёт показания подключенных к нему устройств в «облако» Saures. Доступ к этим данным можно получить в том числе и через API. Именно это я и буду делать в данной статье.
Итак, первым делом я создал решение WaterMeterAutomation и проект библиотеки SauresApi. Для упрощения «общения» с REST API и JSON буду использовать совершенно замечательную библиотеку Flurl. Если быть точнее то Flurl состоит из двух NuGet пакетов: непосредственно Flurl позволяет «строить» (собирать..?) URL, в то время как Flurl.Http — это HTTP-клиент с поддержкой сериализации/десериализации Json. Вот как выглядела первая версия метода LoginAsync в классе Api для вызова метода login API Saures:
public async Task<string> LoginAsync(string email, string password) { var result = await _rootUrl.AppendPathSegment("login") .PostUrlEncodedAsync(new {email, password}) .ReceiveString(); return result; }
Коротко и наглядно!
Чтобы запустить этот код добавлю в решение XUnit проект интеграционных тестов SauresApi.IntegrationTests. Класс TestApi помечу атрибутом [Trait("Category", "IntegrationTests")]
, предварительно добавив категорию «IntegrationTests» в настройки Resharper’а Options
-> Unit Testing
-> General
-> Skip tests from categories
. Это позволит не запускать интеграционные тесты во время запуска всех тестов решения, поскольку конкретно эти тесты мне нужны как средство отладки во время разработки клиента API и не более.
Код теста максимально прост:
[Fact] public async Task TestLogin() { var api = await _api.LoginAsync(_suaresApiLogin, _suaresApiPassword); api.Should().NotBeNullOrEmpty(); }
Поставив точку останова в нужном месте и запустив тест в режиме отладки я получу возможность посмотреть правильно ли работает мой код и результат возвращаемый сервером, который, кстати сказать, имеет следующее содержание:
{ "data": { "api": 1, "role": 1, "sid": "92104027-908a-43dd-b18b-b06b918f696d" }, "errors": [], "status": "ok" }
Признаюсь честно: первую объектную обёртку для этого JSON я накидал ручками, но позже вспомнил, что где-то на просторах интернета видел средство создания классов на основе JSON. Первая же ссылка в Google привела на https://json2csharp.com. В дальнейшем все классы я генерировал на этом сайте и затем менял названия.
Далее поковырявшись в документации Saures API пришёл к выводу, что моя конечная цель это метод /object/meters — именно он возвращает список всех измерительных приборов подключенных к контроллеру с их идентификаторами и показаниями. Но для вызова требуется такой параметр как идентификатор объекта
, получить который можно вызвав метод /user/objects. Реализация двух вышеупомянутых методов была аналогична методу login и в конечном итоге класс Api стал выглядеть следующим образом:
public class Api { private readonly Url _rootUrl; public Api(string host) { _rootUrl = host.AppendPathSegment("1.0"); } public async Task<string> LoginAsync(string email, string password) { var result = await _rootUrl.AppendPathSegment("login") .PostUrlEncodedAsync(new {email, password}) .ReceiveJson<Result<LoginData>>(); result.ThrowExceptionIfAny(); return result.data.sid; } public async Task<UserObject> GetUserObjects(string sid) { var result = await _rootUrl.AppendPathSegment("user").AppendPathSegment("objects") .SetQueryParam("sid", sid) .GetJsonAsync<Result<UserObjectsData>>(); result.ThrowExceptionIfAny(); return result.data.objects[0]; } public async Task<Sensor> GetObjectMeters(string sid, int id) { var result = await _rootUrl.AppendPathSegment("object").AppendPathSegment("meters") .SetQueryParam("sid", sid) .SetQueryParam("id", id) .GetJsonAsync<Result<SensorsData>>(); result.ThrowExceptionIfAny(); return result.data.sensors[0]; } }
Метод ThrowExceptionIfAny бросает ApplicationException если массив errors в результирующем JSON не пустой.
Хранение имени пользователя и пароля в тестовом проекте организовал через переменные окружения (Environment variables). Это позволит использовать эти данные только на моей машине и в то же время они не засветятся в открытом виде в Git или другой системе управления версиями. В будущем этот же подход планирую использовать в главном приложении с поддержкой Docker’а. Финальная версия тестового класса:
[Trait("Category", "IntegrationTests")] public class TestApi { private readonly Api _api; private readonly string _suaresApiLogin; private readonly string _suaresApiPassword; private const string Sid = "0af2b792-2cf9-420c-987e-74e4909c815e"; private const int Id = 12345; public TestApi() { _suaresApiLogin = Environment.GetEnvironmentVariable("SauresApiLogin", EnvironmentVariableTarget.Process); _suaresApiPassword = Environment.GetEnvironmentVariable("SauresApiPassword", EnvironmentVariableTarget.Process); _api = new Api("https://api.saures.ru"); } [Fact] public async Task TestLogin() { var api = await _api.LoginAsync(_suaresApiLogin, _suaresApiPassword); api.Should().NotBeNullOrEmpty(); } [Fact] public async Task TestUserObjects() { var result = await _api.GetUserObjects(Sid); result.id.Should().BeGreaterThan(0); } [Fact] public async Task TestGetObjectMeters() { var result = await _api.GetObjectMeters(Sid, Id); result.meters.Should().HaveCount(4); } }
Sid
— идентификатор сессии получаемый при вызове Login. Id
— идентификатор моего объекта, который вроде бы как постоянен, т.е. метод GetUserObjects можно вызвать единожды.
Отдельно хочу упомянуть про метод Should()
в тестах — он принадлежит ещё одному замечательному инструменту Fluent Assertions, который позволяет сделать утверждения в модульных и интеграционных тестах максимально простыми и удобочитаемыми, но в то же время эффективными. Возможно в будущем я остановлюсь на этой теме подробней, поскольку в данном случае потенциала Fluent Assertions попросту не видно.
P.S. в этой статье впервые я использую подсветку синтаксиса кода. Хочу поблагодарить за это Vicky Agravat за разработку дополнения CodeMirror Blocks.