{"id":371341,"date":"2024-05-21T04:46:26","date_gmt":"2024-05-21T04:46:26","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=371341"},"modified":"-0001-11-30T00:00:00","modified_gmt":"2024-05-21T04:46:26","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=371341","title":{"rendered":"<span>\u041a\u0430\u043a \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 unit-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432: TestHarness<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041f\u0440\u0438\u0432\u0435\u0442 \u0432\u0441\u0435\u043c, \u043d\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441\u043d\u043e\u0432\u0430 \u0410\u043b\u0435\u043a\u0441\u0430\u043d\u0434\u0440 \u0411\u043e\u0431\u0440\u044f\u043a\u043e\u0432, \u0442\u0435\u0445\u043b\u0438\u0434 \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u041c\u0422\u0421 \u0410\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0438. \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u043c \u0446\u0438\u043a\u043b \u0441\u0442\u0430\u0442\u0435\u0439 \u043f\u0440\u043e \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a Apache Flink.<\/p>\n<p>\u041d\u0430\u043f\u043e\u043c\u043d\u044e, \u0432 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u0447\u0430\u0441\u0442\u044f\u0445 \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u043b \u043f\u0440\u043e \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0435 \u043f\u0430\u0439\u043f\u043b\u0430\u0439\u043d\u0430 Kafka-to-Kafka \u0441 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043e\u0447\u043d\u044b\u043c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u043f\u043e\u0442\u043e\u043a\u0430 \u0438 \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0446\u0438\u0435\u0439 \u0441\u043e\u0431\u044b\u0442\u0438\u0439. \u0422\u0430\u043a\u0436\u0435 \u0432 <a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/798667\/\">\u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435<\/a> \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u0430\u043b, \u043a\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u0434\u0438\u043d\u0430\u043c\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0432\u044b\u0445\u043e\u0434\u043d\u043e\u0439 Kafka-\u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u043e\u0433\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f.<\/p>\n<p>\u041d\u0430\u0447\u0438\u043d\u0430\u044f \u0441 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0438 \u043d\u0430\u0447\u043d\u0451\u043c \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0442\u044c, \u043a\u0430\u043a \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0441\u0451 \u043d\u0430\u0448\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Flink + Spring. \u041c\u043d\u043e\u0433\u0438\u0435 \u043e\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u0434\u0445\u043e\u0434\u044b \u0432\u043f\u043e\u043b\u043d\u0435 \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u043c\u044b \u0438 \u0432 \u043b\u044e\u0431\u043e\u043c \u0434\u0440\u0443\u0433\u043e\u043c \u043e\u0431\u044b\u0447\u043d\u043e\u043c Spring-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438, \u043f\u043e\u044d\u0442\u043e\u043c\u0443, \u043d\u0430\u0434\u0435\u044e\u0441\u044c, \u0432\u044b \u043d\u0430\u0439\u0434\u0451\u0442\u0435 \u0434\u043b\u044f \u0441\u0435\u0431\u044f \u0447\u0442\u043e-\u0442\u043e \u043d\u043e\u0432\u043e\u0435.<\/p>\n<p>\u0412 \u0434\u0430\u043d\u043d\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u043c\u044b \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c, \u043a\u0430\u043a \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c stateless- \u0438 stateful-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u044b Flink \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u0439 TestHarness.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/e86\/bc8\/cf0\/e86bc8cf0e127aec76013f1033c29144.jpg\" width=\"1920\" height=\"1080\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/e86\/bc8\/cf0\/e86bc8cf0e127aec76013f1033c29144.jpg\" data-blurred=\"true\"\/><\/figure>\n<details class=\"spoiler\">\n<summary>\u0421\u043f\u0438\u0441\u043e\u043a \u043c\u043e\u0438\u0445 \u043f\u043e\u0441\u0442\u043e\u0432 \u043f\u0440\u043e Flink<\/summary>\n<div class=\"spoiler__content\">\n<ol>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/772898\/\">\u0412\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0432 Apache Flink: \u043e\u0441\u0432\u0430\u0438\u0432\u0430\u0435\u043c \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0445 \u043f\u0440\u0438\u043c\u0435\u0440\u0430\u0445<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/775970\/\">\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043f\u043e\u0434 Apache Flink: \u0441 \u0447\u0435\u0433\u043e \u043d\u0430\u0447\u0430\u0442\u044c?<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/786012\/\">Apache Flink. \u041a\u0430\u043a \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0446\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 \u043f\u043e\u0442\u043e\u043a\u0435 Kafka-to-Kafka?<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/798667\/\">\u0414\u0438\u043d\u0430\u043c\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0432\u044b\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0442\u043e\u043f\u0438\u043a\u0430<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/801693\/\"><strong>\u041a\u0430\u043a \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 unit-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432: Test Harness<\/strong><\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/812905\/\">Unit-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432, Job: Flink MiniCluster<\/a><\/p>\n<\/li>\n<li>\n<p>TBD (\u0432 \u043e\u0447\u0435\u0440\u0435\u0434\u0438)<\/p>\n<\/li>\n<\/ol>\n<\/div>\n<\/details>\n<p>\u0412\u0435\u0441\u044c \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u043c\u044b\u0439 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438 <a href=\"https:\/\/github.com\/AlexanderBobryakov\/flink-spring\">AlexanderBobryakov\/flink-spring<\/a>. \u0412 master-\u0432\u0435\u0442\u043a\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0438\u0442\u043e\u0433\u043e\u0432\u044b\u0439 \u043f\u0440\u043e\u0435\u043a\u0442 \u043f\u043e \u0432\u0441\u0435\u0439 \u0441\u0435\u0440\u0438\u0438 \u0441\u0442\u0430\u0442\u0435\u0439. \u0414\u0430\u043d\u043d\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0440\u0435\u043b\u0438\u0437\u043d\u043e\u0439 \u0432\u0435\u0442\u043a\u0435 \u0441 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c <a href=\"https:\/\/github.com\/AlexanderBobryakov\/flink-spring\/tree\/release\/4_testharness_deduplicator_test\">release\/4_testharness_deduplicator_test<\/a>.<\/p>\n<details class=\"spoiler\">\n<summary>\u041e\u0433\u043b\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0442\u044c\u0438<\/summary>\n<div class=\"spoiler__content\">\n<ol>\n<li>\n<p><a href=\"#1\">\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0442\u0435\u0441\u0442\u043e\u0432<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#2\">\u0410\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u044f EntityTestBuilder<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#3\">\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 stateless Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#4\">\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 stateful Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#5\">\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e TestHarness-\u043a\u043b\u0430\u0441\u0441\u043e\u0432<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#6\">\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e TestHarness-\u043a\u043b\u0430\u0441\u0441\u043e\u0432<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#7\">\u0412\u044b\u0432\u043e\u0434<\/a><\/p>\n<\/li>\n<\/ol>\n<\/div>\n<\/details>\n<p><a class=\"anchor\" name=\"1\" id=\"1\"><\/a><\/p>\n<h2>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0442\u0435\u0441\u0442\u043e\u0432<\/h2>\n<p>\u041d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u0441\u043f\u043e\u0439\u043b\u0435\u0440: \u043a \u043a\u043e\u043d\u0446\u0443 \u0441\u0442\u0430\u0442\u044c\u0438 \u043c\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u043c \u0432\u043e\u0442 \u0442\u0430\u043a\u0443\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u0442\u0435\u0441\u0442\u043e\u0432: <\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/ef2\/771\/2ce\/ef27712ce2dd113d7f1fca4814246f47.png\" width=\"702\" height=\"588\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/ef2\/771\/2ce\/ef27712ce2dd113d7f1fca4814246f47.png\"\/><\/figure>\n<p>\u041c\u044b \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u043f\u0435\u0440\u0432\u044b\u0435 \u0442\u0435\u0441\u0442\u044b \u043d\u0430 stateless-\u0444\u0438\u043b\u044c\u0442\u0440, stateful-\u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0442\u043e\u0440 \u0438 \u043d\u0430 stateless-\u0441\u043f\u043b\u0438\u0442\u0435\u0440\u0430\u0442\u043e\u0440 (\u0443\u0447\u0438\u0442\u044b\u0432\u0430\u044e\u0449\u0438\u0439 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0435 Flink-\u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u0438).<\/p>\n<p><a class=\"anchor\" name=\"2\" id=\"2\"><\/a><\/p>\n<h2>\u0410\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u044f EntityTestBuilder<\/h2>\n<p>\u0412 \u043d\u0430\u0448\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0431\u0438\u0437\u043d\u0435\u0441-DTO, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 ClickMessage \u0438 ProductMessage, \u0438 \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043d\u043e\u0432\u044b\u0435. \u0412 \u0441\u0430\u043c\u0438\u0445 \u0442\u0435\u0441\u0442\u0430\u0445 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0445 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0435\u0432. \u041d\u043e \u043f\u043e\u0434\u0443\u043c\u0430\u0435\u043c \u043e\u0431 \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0435: \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043c \u0435\u0434\u0438\u043d\u043e\u0435 \u043c\u0435\u0441\u0442\u043e, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0431\u0443\u0434\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043a\u0430\u0436\u0434\u043e\u0435 DTO. \u0412\u0435\u0434\u044c \u0432 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u044f\u0445 \u043c\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u0435\u0439, \u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0433\u0443\u0442 \u0438\u043c\u0435\u0442\u044c \u0431\u0438\u0437\u043d\u0435\u0441-\u0446\u0435\u043d\u043d\u043e\u0441\u0442\u044c.<\/p>\n<p>\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0432 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441: <\/p>\n<pre><code class=\"java\">public interface EntityTestBuilder&lt;T> {     T build(); }<\/code><\/pre>\n<p>\u0414\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432 \u0443 \u043d\u0435\u0433\u043e \u0442\u0430\u043a\u0436\u0435 \u0431\u0443\u0434\u0443\u0442 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u043e\u0434 \u043a\u0430\u0436\u0434\u0443\u044e \u043d\u0430\u0448\u0443 DTO. \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043f\u0440\u0438\u043c\u0435\u0440 \u0434\u043b\u044f ClickMessage:<\/p>\n<pre><code class=\"java\">@With @AllArgsConstructor(access = PUBLIC) @NoArgsConstructor(access = PRIVATE) public class ClickMessageTestBuilder implements EntityTestBuilder&lt;ClickMessage> {    private UUID userId = UUID.randomUUID();    private String object = \"test_object\";    private Platform platform = Platform.Enum.APP;    private String productName = \"test_productName\";    private String productTopic = \"test_productTopic\";    private Long timestamp = 123L;    private Map&lt;String, Object> data = new HashMap&lt;>() {{        put(\"field_1\", \"value_1\");        put(\"field_2\", \"value_2\");    }};     public static ClickMessageTestBuilder aClickMessage() {        return new ClickMessageTestBuilder();    }     @Override    public ClickMessage build() {        return ClickMessage.builder()                   .userId(userId)                   .object(object)                   .platform(platform)                   .productName(productName)                   .productTopic(productTopic)                   .timestamp(timestamp)                   .data(data)                   .build();    } }<\/code><\/pre>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043c\u044b \u0432 \u043e\u0434\u043d\u043e\u043c \u043c\u0435\u0441\u0442\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u0432\u0441\u0435 \u0434\u0435\u0444\u043e\u043b\u0442\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0435\u0439, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u043c\u0435\u0442\u043e\u0434-\u0431\u0438\u043b\u0434\u0435\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u043c \u0432\u044b\u0437\u044b\u0432\u0430\u0442\u044c \u0432 \u0442\u0435\u0441\u0442\u0430\u0445 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u0430 ClickMessage. \u041f\u0440\u0438 \u044d\u0442\u043e\u043c \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0438 lombok \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0442\u044c \u0434\u0435\u0444\u043e\u043b\u0442\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0435\u0442\u043e\u0434\u043e\u0432 withXXX(&#8230;), \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440:<\/p>\n<pre><code class=\"java\">aClickMessage().withPlatform(APP).build()<\/code><\/pre>\n<p><a class=\"anchor\" name=\"3\" id=\"3\"><\/a><\/p>\n<h2>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 stateless Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432<\/h2>\n<p>\u041d\u0430\u0447\u043d\u0451\u043c \u0441 \u0441\u0430\u043c\u044b\u0445 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0442\u0435\u0441\u0442\u043e\u0432 \u043d\u0430 stateless-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u044b. \u041d\u0430\u043f\u043e\u043c\u043d\u044e, \u0447\u0442\u043e \u043e\u043d\u0438 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f. \u0418\u043c \u043d\u0435\u0432\u0430\u0436\u043d\u043e, \u043a\u0430\u043a\u0438\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043e\u043d\u0438 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043b\u0438 \u0440\u0430\u043d\u0435\u0435 \u2014 \u043a\u0430\u0436\u0434\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u043e\u0442 \u0434\u0440\u0443\u0433\u0438\u0445. \u0422\u0430\u043a\u043e\u0439 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043a\u0430\u043a \u043e\u0431\u044b\u0447\u043d\u044b\u0439 Java-\u043a\u043b\u0430\u0441\u0441, \u0438 \u0432 \u0435\u0433\u043e \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u043d\u0435\u0442 \u043d\u0438\u0447\u0435\u0433\u043e \u0441\u043b\u043e\u0436\u043d\u043e\u0433\u043e.<\/p>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430\u0448 \u0444\u0438\u043b\u044c\u0442\u0440, \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0449\u0438\u0439 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0441 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c \u043f\u043e\u043b\u044f platform:<\/p>\n<pre><code class=\"java\">public class ClickMessageWithPlatformFilter implements FilterFunction&lt;ClickMessage> {    private static final long serialVersionUID = 1L;     @Override    public boolean filter(ClickMessage message) {        return Optional.ofNullable(message.getPlatform())                   .map(platform -> WEB.equals(platform) || APP.equals(platform))                   .orElse(false);    } }<\/code><\/pre>\n<p>\u041f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e JUnit 5 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0438 @ParameterizedTest. \u042d\u0442\u0430 \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u0443 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0442 \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u0432\u0442\u043e\u0440\u043e\u0439 Source-\u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0438 (@MethodSource, @ValueSource, @CsvSource&#8230;):<\/p>\n<pre><code class=\"java\">class ClickMessageWithPlatformFilterUnitTest {     @ParameterizedTest    @MethodSource(\"clickMessagesByPlatform\")    void shouldFilterMessageByPlatform(boolean expectedFilter, ClickMessage message) {        final var filter = new ClickMessageWithPlatformFilter();        final var filterResult = filter.filter(message);        assertEquals(expectedFilter, filterResult, format(\"Unexpected filter result for message: %s\", message));    }     private static Stream&lt;Arguments> clickMessagesByPlatform() {        return Stream.of(            arguments(true, aClickMessage().withPlatform(APP).build()),            arguments(true, aClickMessage().withPlatform(WEB).build()),            arguments(false, aClickMessage().withPlatform(Platform.of(\"unknown\")).build()),            arguments(false, aClickMessage().withPlatform(Platform.of(\"\")).build()),            arguments(false, aClickMessage().withPlatform(null).build())        );    } }<\/code><\/pre>\n<ul>\n<li>\n<p>\u0432 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u043c \u043c\u0435\u0442\u043e\u0434\u0435 clickMessagesByPlatform \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u043f\u0435\u0440\u0435\u0447\u0435\u043d\u044c \u0432\u0441\u0435\u0445 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0439 \u0432\u0445\u043e\u0434\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0430 .boolean expectedFilter \u2014 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0432\u044b\u0437\u043e\u0432\u0430 \u0444\u0438\u043b\u044c\u0442\u0440\u0430 true\/false<\/p>\n<\/li>\n<li>\n<p>ClickMessage message \u2014 \u0441\u0430\u043c\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438 (\u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u044d\u0442\u043e\u0433\u043e \u043a\u043b\u0430\u0441\u0441\u0430)<\/p>\n<\/li>\n<\/ul>\n<p>\u0422\u0435\u0441\u0442 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0442\u044c\u0441\u044f 5 \u0440\u0430\u0437. \u0412 \u0441\u0430\u043c\u043e\u043c \u0442\u0435\u0441\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0451\u043c \u043e\u0431\u044a\u0435\u043a\u0442 \u0444\u0438\u043b\u044c\u0442\u0440\u0430 \u0438 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u043c \u043c\u0435\u0442\u043e\u0434 filter \u0434\u043b\u044f \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0435\u0433\u043e \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f \u0441 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u043c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u043c.<\/p>\n<p>\u0422\u0430\u043a \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u0430\u0441\u0441\u044b \u0434\u043b\u044f \u043c\u0435\u0442\u043e\u0434\u043e\u0432 map, flatMap, process \u0438 \u0442.\u00a0\u0434. \u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u0438\u043d\u043e\u0433\u0434\u0430 \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u044c\u0441\u044f \u0437\u0430\u043c\u043e\u043a\u0430\u0442\u044c \u043a\u0430\u043a\u043e\u0439-\u043d\u0438\u0431\u0443\u0434\u044c \u043e\u0431\u044a\u0435\u043a\u0442 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Mockito, \u043d\u043e \u044d\u0442\u043e \u043d\u0438\u0447\u0435\u043c \u043d\u0435 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f.<\/p>\n<p><a class=\"anchor\" name=\"4\" id=\"4\"><\/a><\/p>\n<h2>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 stateful Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432<\/h2>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u0435\u0440\u0435\u0439\u0434\u0451\u043c \u043a \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044e stateful-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432. \u041e\u043d\u0438 \u0442\u0435\u0441\u043d\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u044b \u0441\u043e \u0441\u0440\u0435\u0434\u043e\u0439 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Flink, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043c\u043e\u0433\u0443\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435, \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u043e\u0435 Flink, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043a\u0430\u043a\u0438\u0435-\u043d\u0438\u0431\u0443\u0434\u044c \u0442\u0430\u0439\u043c\u0435\u0440\u044b \u0438 \u0442.\u00a0\u0434. \u0412 \u044d\u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043f\u043e\u0434\u0445\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u044f \u043e\u043f\u0438\u0441\u0430\u043b \u0432\u044b\u0448\u0435, \u043d\u0435 \u043f\u043e\u0434\u043e\u0439\u0434\u0451\u0442. \u041d\u0430 \u044d\u0442\u043e\u0442 \u0440\u0430\u0437 \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u044b \u0431\u043e\u043b\u0435\u0435 \u0443\u043c\u043d\u044b\u0435 \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0433\u0443\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f Flink. \u0422\u0430\u043a\u0438\u0435 \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u0438 \u0435\u0441\u0442\u044c \u0432 <a href=\"https:\/\/nightlies.apache.org\/flink\/flink-docs-release-1.17\/docs\/dev\/datastream\/testing\/#unit-testing-stateful-or-timely-udfs--custom-operators\">\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438<\/a> \u043a \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044e. <\/p>\n<p>\u0422\u0430\u043a\u0436\u0435 \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0430\u043a \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u043c\u044b\u0435 TestHarness-\u043a\u043b\u0430\u0441\u0441\u044b. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u043c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438:<\/p>\n<pre><code class=\"yaml\">testImplementation \"org.apache.flink:flink-streaming-java:${flinkVersion}:tests\" testImplementation \"org.apache.flink:flink-test-utils:${flinkVersion}\"<\/code><\/pre>\n<p>\u041f\u0435\u0440\u0432\u0430\u044f \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c \u0434\u0430\u0451\u0442 \u043d\u0430\u043c \u0434\u043e\u0441\u0442\u0443\u043f \u043a\u043e \u0432\u0441\u0435\u043c \u043e\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u043c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 TestHarness-\u043a\u043b\u0430\u0441\u0441\u0430\u043c, \u0430 \u0432\u0442\u043e\u0440\u0430\u044f \u2014 \u043a\u043e \u043c\u043d\u043e\u0433\u0438\u043c \u0443\u0434\u043e\u0431\u043d\u044b\u043c \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u044f\u043c \u0432 \u0432\u0438\u0434\u0435 Flink MiniCluster, TestEnvironment \u0438 \u0442.\u00a0\u0434.<\/p>\n<p><a class=\"anchor\" name=\"5\" id=\"5\"><\/a><\/p>\n<h2>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e TestHarness-\u043a\u043b\u0430\u0441\u0441\u043e\u0432<\/h2>\n<p>TestHarness \u2014 \u044d\u0442\u043e \u043a\u043b\u0430\u0441\u0441\u044b \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043d\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0439 \u043b\u043e\u0433\u0438\u043a\u0438 Flink: watermark, \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f, \u0442\u0430\u0439\u043c\u0435\u0440\u044b \u0438 \u0442.\u00a0\u0434.<\/p>\n<p>\u0421\u0435\u0439\u0447\u0430\u0441 \u043e\u0431\u044a\u044f\u0441\u043d\u044e \u043d\u0430 \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u043e\u0431\u044b\u0447\u043d\u043e\u0433\u043e stateless-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0430. \u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0442\u0435\u0441\u0442 \u043d\u0430\u0434 \u043d\u0430\u0448\u0438\u043c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u043c \u0440\u0430\u043d\u0435\u0435 \u0441\u043f\u043b\u0438\u0442\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0432\u0445\u043e\u0434\u043d\u044b\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u043f\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u043c \u043f\u043e\u0431\u043e\u0447\u043d\u044b\u043c \u0432\u044b\u0445\u043e\u0434\u043d\u044b\u043c \u043f\u043e\u0442\u043e\u043a\u0430\u043c (\u043f\u043e OutputTag) \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043f\u0440\u0435\u0434\u0438\u043a\u0430\u0442\u043e\u0432:<\/p>\n<pre><code class=\"java\">@RequiredArgsConstructor public class StreamSpliterator&lt;I> extends ProcessFunction&lt;I, Object> {    private static final long serialVersionUID = 1L;     private final Map&lt;OutputTag&lt;I>, SerializablePredicate&lt;I>> predicatesByTags;    private final OutputTag&lt;I> defaultTag;     @Override    public void processElement(I value, ProcessFunction&lt;I, Object>.Context ctx, Collector&lt;Object> out) {        for (Map.Entry&lt;OutputTag&lt;I>, SerializablePredicate&lt;I>> entry : predicatesByTags.entrySet()) {            final var predicate = entry.getValue();            if (predicate.test(value)) {                final var tag = entry.getKey();                ctx.output(tag, value);                return;            }        }        ctx.output(defaultTag, value);    } }<\/code><\/pre>\n<p>\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0435 \u043e\u0431\u044a\u044f\u0441\u043d\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u043c\u043e\u0436\u043d\u043e <a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/786012\/\">\u0433\u043b\u044f\u043d\u0443\u0442\u044c \u0442\u0443\u0442<\/a>. \u041e\u0441\u043d\u043e\u0432\u043d\u0430\u044f \u0441\u043b\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u2014 \u0437\u0430\u043f\u0438\u0441\u044c \u0432 \u043e\u0431\u044a\u0435\u043a\u0442 \u043a\u043b\u0430\u0441\u0441\u0430 ProcessFunction.&lt;I, Object>Context \u043f\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u043e\u043c\u0443 \u0442\u0435\u0433\u0443 \u0432 \u0441\u0442\u0440\u043e\u043a\u0435 ctx.output(tag, value). \u041c\u043e\u0436\u043d\u043e, \u043a\u043e\u043d\u0435\u0447\u043d\u043e, \u0437\u0430\u043c\u043e\u043a\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u043a\u043b\u0430\u0441\u0441 \u0438 \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 unit-\u0442\u0435\u0441\u0442, \u043d\u043e \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043d\u0435 \u043e\u0447\u0435\u043d\u044c \u0445\u043e\u0440\u043e\u0448\u043e. \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0441\u044f, \u0447\u0442\u043e \u0442\u0435\u0441\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0437\u043d\u0430\u0442\u044c \u043e \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0439 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0435\/\u043b\u043e\u0433\u0438\u043a\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u043e\u0433\u043e \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0430. \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0431\u043e\u043b\u0435\u0435 \u043a\u0440\u0430\u0441\u0438\u0432\u044b\u0439 \u0442\u0435\u0441\u0442 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0432\u0441\u043f\u043e\u043c\u043e\u0433\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0445 TestHarness-\u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u0439:<\/p>\n<pre><code class=\"java\">class StreamSpliteratorUnitTest {     @Test    void shouldSplitStreamByPredicates() throws Exception {        final var webTag = new OutputTag&lt;>(\"web_tag\", TypeInformation.of(String.class));        final var mobileTag = new OutputTag&lt;>(\"mobile_tag\", TypeInformation.of(String.class));        final var unknownTag = new OutputTag&lt;>(\"unknown_tag\", TypeInformation.of(String.class));        final var events = List.of(            new StreamRecord&lt;>(\"web\"),            new StreamRecord&lt;>(\"mobile\"),            new StreamRecord&lt;>(\"test\"),            new StreamRecord&lt;>(\"web\")        );        final var spliterator = new StreamSpliterator&lt;>(            Map.of(                webTag, \"web\"::equals,                mobileTag, \"mobile\"::equals            ),            unknownTag        );        @Cleanup final var testHarness = ProcessFunctionTestHarnesses.forProcessFunction(spliterator);         testHarness.processElements(events);         assertTrue(testHarness.getRecordOutput().isEmpty(), \"Output stream has unexpected events\");        assertEquals(2, testHarness.getSideOutput(webTag).size(),            format(\"Unexpected events in 'web' output stream: %s\", new ArrayList&lt;>(testHarness.getSideOutput(webTag))));        assertEquals(1, testHarness.getSideOutput(mobileTag).size(),            format(\"Unexpected events in 'mobile' output stream: %s\", new ArrayList&lt;>(testHarness.getSideOutput(mobileTag))));        assertEquals(1, testHarness.getSideOutput(unknownTag).size(),            format(\"Unexpected events in 'default' output stream: %s\", new ArrayList&lt;>(testHarness.getSideOutput(unknownTag))));    } }<\/code><\/pre>\n<p>\u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u043c \u0442\u0440\u0438 \u0442\u0435\u0433\u0430: web, mobile, unknown. \u041f\u043e \u043d\u0438\u043c \u043c\u044b \u043e\u0436\u0438\u0434\u0430\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0432\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430. \u0414\u0430\u043b\u0435\u0435 \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u044b \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u2014 events, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0435\u043c \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u043d\u0430 \u0432\u0445\u043e\u0434 \u0441\u043f\u043b\u0438\u0442\u0435\u0440\u0430\u0442\u043e\u0440\u0430 StreamSpliterator. \u041c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u043a\u043b\u0430\u0441\u0441 StreamRecord \u2014 \u044d\u0442\u043e \u0431\u0430\u0437\u043e\u0432\u0430\u044f \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u044f \u0432 Flink. \u041e\u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f \u0432\u043d\u0443\u0442\u0440\u0438 \u0441\u0442\u0440\u0438\u043c\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0438\u043c\u0435\u043d\u043d\u043e \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u0431\u044a\u0435\u043a\u0442. \u042d\u0442\u043e\u0442 \u043a\u043b\u0430\u0441\u0441 \u2014 \u043e\u0434\u043d\u0430 \u0438\u0437 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0439 \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0442\u043d\u043e\u0433\u043e StreamElement. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0434\u0440\u0443\u0433\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u2014 \u043a\u043b\u0430\u0441\u0441 Watermark. \u041e\u043d \u043c\u043e\u0436\u0435\u0442 \u0442\u0430\u043a\u0436\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c\u0441\u044f \u0432\u043d\u0443\u0442\u0440\u0438 \u043f\u043e\u0442\u043e\u043a\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c \u0441\u043e\u0431\u044b\u0442\u0438\u0435\u043c \u0434\u043b\u044f \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u044f \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432. \u041f\u043e \u0441\u0443\u0442\u0438 \u0432 \u0442\u0435\u0441\u0442\u0435 \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u043b\u0438 \u0447\u0435\u0442\u044b\u0440\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0441 \u0442\u0438\u043f\u043e\u043c String. \u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u0438\u0434\u0451\u0442 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0441\u043f\u043b\u0438\u0442\u0435\u0440\u0430\u0442\u043e\u0440\u0430 new StreamSpliterator: \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0441 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c \u043c\u044b \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u043c \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u043f\u043e\u0431\u043e\u0447\u043d\u044b\u0439 \u0432\u044b\u0445\u043e\u0434 \u043f\u043e \u0442\u0435\u0433\u0443 \u0441\u043e\u0433\u043b\u0430\u0441\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u043f\u0440\u0435\u0434\u0438\u043a\u0430\u0442\u0443 (\u0434\u043b\u044f \u043f\u0440\u043e\u0441\u0442\u043e\u0442\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c equals \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f).<\/p>\n<p>\u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u0432\u0441\u0442\u0443\u043f\u0430\u044e\u0442 \u0432 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 TestHarness-\u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u0438. \u041c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0442\u0438\u043b\u0438\u0442\u043d\u044b\u0439 \u043a\u043b\u0430\u0441\u0441 ProcessFunctionTestHarnesses, \u0443 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043a\u043b\u0430\u0441\u0441\u0430 AbstractStreamOperatorTestHarness. \u041e\u043d, \u0432 \u0441\u0432\u043e\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u044c, \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043d\u0430\u0434 \u0441\u0442\u0440\u0438\u043c\u043e\u043c, \u0431\u0443\u0434\u0442\u043e \u044d\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442 \u0441\u0430\u043c Flink. \u0422\u0430\u043a \u043a\u0430\u043a \u043d\u0430\u0448 \u0441\u043f\u043b\u0438\u0442\u0435\u0440\u0430\u0442\u043e\u0440 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442 ProcessFunction, \u0442\u043e \u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u043c\u0435\u0442\u043e\u0434\u043e\u043c forProcessFunction(&#8230;) \u0443\u0442\u0438\u043b\u0438\u0442\u043d\u043e\u0433\u043e \u043a\u043b\u0430\u0441\u0441\u0430 ProcessFunctionTestHarnesses. \u0422\u0435\u043f\u0435\u0440\u044c \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 testHarnsess, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432. \u0417\u0430\u043c\u0435\u0442\u044c\u0442\u0435, \u043e\u043d \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442 AutoCloseable, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u044b \u0441\u0442\u0430\u0432\u0438\u043c \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u044e \u043b\u043e\u043c\u0431\u043e\u043a\u0430 @Cleanup \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 close()-\u043c\u0435\u0442\u043e\u0434\u0430. <\/p>\n<p>\u0418 \u0435\u0449\u0451 \u043f\u0430\u0440\u0430 \u043c\u0435\u0442\u043e\u0434\u043e\u0432:<\/p>\n<ul>\n<li>\n<p>processElements() \u2014 \u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0432\u0441\u0435\u0445 \u0432\u0445\u043e\u0434\u043d\u044b\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439<\/p>\n<\/li>\n<li>\n<p>getRecordOutput \u2014 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0432\u044b\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438, \u0447\u0442\u043e \u0432\u044b\u0445\u043e\u0434\u043d\u044b\u0445 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u043d\u0435\u0442. \u0422\u0430\u043a \u043a\u0430\u043a \u043d\u0430\u0448 \u0441\u043f\u043b\u0438\u0442\u0435\u0440\u0430\u0442\u043e\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u0437\u0430\u043a\u0438\u043d\u0443\u0442\u044c \u0432\u0441\u0435 \u043b\u0438\u0448\u043d\u0438\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u043d\u0435 \u043f\u043e\u043f\u0430\u0432\u0448\u0438\u0435 \u043f\u043e\u0434 \u0443\u0441\u043b\u043e\u0432\u0438\u044f \u0432\u0441\u0435\u0445 \u043f\u0440\u0435\u0434\u0438\u043a\u0430\u0442\u043e\u0432, \u0432 \u043f\u043e\u0431\u043e\u0447\u043d\u044b\u0439 \u0432\u044b\u0445\u043e\u0434\u043d\u043e\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0441 \u0442\u0435\u0433\u043e\u043c unknown. \u0418 \u0432\u043e\u0442 \u043a\u0430\u043a \u0440\u0430\u0437 \u043f\u043e \u044d\u0442\u043e\u043c\u0443 \u0442\u0435\u0433\u0443 \u043c\u044b \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u0432\u044b\u0445\u043e\u0434\u043d\u044b\u0435 \u043f\u043e\u0442\u043e\u043a\u0438 \u0447\u0435\u0440\u0435\u0437 \u043c\u0435\u0442\u043e\u0434 getSideOutput()<\/p>\n<\/li>\n<\/ul>\n<p>\u041f\u043e\u0434\u043e\u0431\u043d\u0430\u044f TestHarness-\u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u044f \u043e\u0447\u0435\u043d\u044c \u0443\u0434\u043e\u0431\u043d\u0430, \u0438 \u0432\u044b\u0437\u043e\u0432 \u0435\u0451 \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u043c\u043e\u0436\u0435\u0442 \u044d\u043c\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443, \u0431\u0443\u0434\u0442\u043e \u044d\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442 \u0441\u0430\u043c Flink-\u043a\u043b\u0430\u0441\u0442\u0435\u0440.<\/p>\n<p><a class=\"anchor\" name=\"6\" id=\"6\"><\/a><\/p>\n<h2>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e TestHarness-\u043a\u043b\u0430\u0441\u0441\u043e\u0432<\/h2>\n<p>\u041f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0439 \u0442\u0435\u0441\u0442 \u0431\u044b\u043b \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u044b\u043c, \u0430 \u0442\u0435\u043f\u0435\u0440\u044c \u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u0432\u0437\u0433\u043b\u044f\u043d\u0435\u043c \u043d\u0430 \u0442\u0435\u0441\u0442 \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u043e\u0433\u043e stateful-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0430 \u2014 \u043d\u0430\u0448\u0435\u0433\u043e \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0438\u0437 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u0441\u0442\u0430\u0442\u0435\u0439. \u041d\u0430\u043f\u043e\u043c\u043d\u044e \u0435\u0433\u043e \u043a\u043e\u0434:<\/p>\n<pre><code class=\"java\">public class Deduplicator&lt;T> extends RichFlatMapFunction&lt;T, T> {    private static final long serialVersionUID = 1L;     private final Time ttl;    private ValueState&lt;Boolean> keyHasBeenSeen;     public Deduplicator(Time ttl) {        this.ttl = notNull(ttl, \"Deduplicator TTL is null\");    }     @Override    public void open(Configuration parameters) {        final var descriptor = new ValueStateDescriptor&lt;>(\"keyHasBeenSeen\", Types.BOOLEAN);        final var ttlConfig = StateTtlConfig                                  .newBuilder(ttl)                                  .setUpdateType(UpdateType.OnCreateAndWrite)                                  .setStateVisibility(NeverReturnExpired)                                  .cleanupFullSnapshot()                                  .build();        descriptor.enableTimeToLive(ttlConfig);        keyHasBeenSeen = getRuntimeContext().getState(descriptor);    }     @Override    public void flatMap(T event, Collector&lt;T> out) throws Exception {        if (keyHasBeenSeen.value() == null) {            out.collect(event);            keyHasBeenSeen.update(true);        }    } }<\/code><\/pre>\n<p>\u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0435 \u043e\u0431\u044a\u044f\u0441\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u0431\u044b\u043b\u043e <a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/786012\/\">\u0432\u043e\u0442 \u0432 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435<\/a>. \u0410 \u0441\u0435\u0439\u0447\u0430\u0441 \u043c\u044b \u0441\u043e\u0441\u0440\u0435\u0434\u043e\u0442\u043e\u0447\u0438\u043c\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0442\u0435\u0441\u0442\u0435. \u0415\u0441\u043b\u0438 \u0432\u043a\u0440\u0430\u0442\u0446\u0435, \u0442\u043e \u0443 \u043d\u0430\u0441 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 ValueState&lt;Boolean>, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0444\u043b\u0430\u0433 true \u0432 \u0441\u043b\u0443\u0447\u0430\u044f\u0445, \u043a\u043e\u0433\u0434\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u0435\u0442\u0441\u044f \u0432\u043f\u0435\u0440\u0432\u044b\u0435. \u041f\u0440\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u043c \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043a\u043b\u044e\u0447\u043e\u043c \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043d\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043b\u044c\u0448\u0435 (\u0434\u0435\u0434\u0443\u043f\u043b\u0438\u0446\u0438\u0440\u0443\u0435\u0442\u0441\u044f), \u0442\u0430\u043a \u043a\u0430\u043a \u0432 \u0440\u0430\u043c\u043a\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e TTL \u043c\u044b \u0443\u0436\u0435 \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u043b\u0438 \u0435\u0433\u043e \u0440\u0430\u043d\u0435\u0435.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u0435\u0440\u0435\u0439\u0434\u0451\u043c \u043d\u0435\u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u043a \u0442\u0435\u0441\u0442\u0443. \u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0435\u0433\u043e \u043f\u043e \u0447\u0430\u0441\u0442\u044f\u043c:<\/p>\n<pre><code class=\"java\">class DeduplicatorUnitTest_byTestHarness {    private final Time TTL = Time.milliseconds(10);    private KeyedOneInputStreamOperatorTestHarness&lt;String, String, String> testHarness;     @BeforeEach    @SneakyThrows    void init() {        final var deduplicator = new Deduplicator&lt;String>(TTL);        testHarness = new KeyedOneInputStreamOperatorTestHarness&lt;>(            new StreamFlatMap&lt;>(deduplicator),            value -> value,            Types.STRING        );        testHarness.open();    }     @AfterEach    @SneakyThrows    void clear() {        testHarness.close();    } ... }<\/code><\/pre>\n<p>\u0412 \u0441\u0430\u043c\u043e\u043c \u043d\u0430\u0447\u0430\u043b\u0435 \u043c\u044b \u0437\u0430\u0434\u0430\u0451\u043c TTL = 10 \u043c\u0441. \u0422\u0430\u043a\u0436\u0435 \u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u044f\u043c\u0438 BeforeEach\/AfterEach \u0434\u043b\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 TestHarness \u0438 \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f \u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432. \u041d\u0430 \u044d\u0442\u043e\u0442 \u0440\u0430\u0437 \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u044e KeyedOneInputStreamOperatorTestHarness, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0442\u043e\u0440 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432 \u0440\u0430\u0437\u0440\u0435\u0437\u0435 \u043a\u043b\u044e\u0447\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0438 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043f\u0435\u0440\u0435\u0434 \u0441\u043e\u0431\u043e\u0439 \u0432\u044b\u0437\u043e\u0432 keyBy-\u043c\u0435\u0442\u043e\u0434\u0430 \u0432 \u043f\u0430\u0439\u043f\u043b\u0430\u0439\u043d\u0435. \u0412 \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u043e\u0440 \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u043c:<\/p>\n<ol>\n<li>\n<p>\u041d\u0430\u0448 \u043e\u0431\u044a\u0435\u043a\u0442 \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0442\u043e\u0440\u0430.<\/p>\n<\/li>\n<li>\n<p>\u041b\u044f\u043c\u0431\u0434\u0443 \u0434\u043b\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f (\u0430\u043d\u0430\u043b\u043e\u0433 keyBy-\u0432\u044b\u0437\u043e\u0432\u0430 \u0432 \u043f\u0430\u0439\u043f\u043b\u0430\u0439\u043d\u0435).<\/p>\n<\/li>\n<li>\n<p>\u0422\u0438\u043f \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0434\u043b\u044f \u043f\u043e\u043c\u043e\u0449\u0438 Flink. <\/p>\n<\/li>\n<\/ol>\n<p>\u0412\u0430\u0436\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442: \u0432\u044b\u0437\u043e\u0432 \u043c\u0435\u0442\u043e\u0434\u0430 open, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u043e\u0431\u044a\u0435\u043a\u0442 TestHarness \u0441 \u0443\u0447\u0451\u0442\u043e\u043c \u0435\u0433\u043e \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f ValueState.<\/p>\n<p>\u0414\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0438\u0439 \u0442\u0435\u0441\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442, \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u043b\u0438 \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0446\u0438\u044f: \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u043d\u0430 \u0432\u0445\u043e\u0434 \u043e\u0434\u043d\u0438 \u0438 \u0442\u0435 \u0436\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0434\u0432\u0430\u0436\u0434\u044b, \u043d\u043e \u043d\u0430 \u0432\u044b\u0445\u043e\u0434\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0432 \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u043c \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0435. \u041d\u0430\u0448 \u043c\u0435\u0442\u043e\u0434 provideUniqueEvents (\u0435\u0433\u043e \u043a\u043e\u0434 \u043d\u0438\u0436\u0435) \u043a\u0430\u043a \u0440\u0430\u0437 \u0431\u0443\u0434\u0435\u0442 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0430 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0445 String-\u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0441 \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0432 ParametrizedTest:<\/p>\n<pre><code class=\"java\">private static Stream&lt;Arguments> provideUniqueEvents() {    return Stream.of(        arguments(List.of(new StreamRecord&lt;>(\"key_1\"), new StreamRecord&lt;>(\"key_2\")))    ); }<\/code><\/pre>\n<p>\u0422\u0435\u0441\u0442 \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"java\">@SneakyThrows @ParameterizedTest @MethodSource(\"provideUniqueEvents\") void shouldDeduplicateMessagesByTtl(List&lt;StreamRecord&lt;String>> events) {    testHarness.processElements(events);    testHarness.setStateTtlProcessingTime(TTL.toMilliseconds() - 1);    testHarness.processElements(events);     final var outputEvents = testHarness.getOutput();    assertEquals(events.size(), outputEvents.size(),        format(\"Unexpected number of events after deduplication. Output events: %s\", outputEvents));    assertEquals(events, new ArrayList&lt;>(outputEvents), \"Unexpected events after deduplication\"); }<\/code><\/pre>\n<p>\u0412 \u0442\u0435\u0441\u0442\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043f\u0435\u0440\u0432\u044b\u0439 \u0440\u0430\u0437 \u0447\u0435\u0440\u0435\u0437 processElements, \u0430 \u0434\u0430\u043b\u0435\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u0441\u043f\u043e\u043c\u043e\u0433\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u044d\u043c\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f TTL. \u0412\u044b\u0437\u043e\u0432 setStateTtlProcessingTime \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0443 \u0434\u0443\u043c\u0430\u0442\u044c, \u0431\u0443\u0434\u0442\u043e \u043f\u0440\u043e\u0448\u043b\u043e \u0443\u0436\u0435 (TTL.toMilliseconds() \u2013 1) \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u043e\u0441\u043b\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439. \u0422\u0430\u043a \u043a\u0430\u043a \u0441\u0430\u043c TTL \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0430 1 \u043c\u0441, \u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0434\u043e\u043b\u0436\u043d\u044b \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u044b \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u0438\u0445 \u043d\u0430 \u0432\u0445\u043e\u0434 \u0435\u0449\u0451 \u0440\u0430\u0437 \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c, \u0447\u0442\u043e \u043d\u0430 \u0432\u044b\u0445\u043e\u0434\u0435 testHarness.getOutput() \u0438\u0445 \u043d\u0435 \u0441\u0442\u0430\u043b\u043e \u0432 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430 \u0431\u043e\u043b\u044c\u0448\u0435.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0430\u043c \u043d\u0443\u0436\u0435\u043d \u0442\u0435\u0441\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442, \u0447\u0442\u043e \u0441\u043f\u0443\u0441\u0442\u044f TTL \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043c\u043e\u0433\u0443\u0442 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c\u0441\u044f \u0432\u043d\u043e\u0432\u044c:<\/p>\n<pre><code class=\"java\">@SneakyThrows @ParameterizedTest @MethodSource(\"provideUniqueEvents\") void shouldNotDeduplicateBecauseOfTtlExpired(List&lt;StreamRecord&lt;String>> events) {    testHarness.processElements(events);    testHarness.setStateTtlProcessingTime(TTL.toMilliseconds() + 1);    testHarness.processElements(events);     final var outputEvents = testHarness.getOutput();    assertEquals(events.size() * 2, outputEvents.size(),        format(\"Unexpected number of events after deduplication. Output events: %s\", outputEvents));    final var expectedEvents = new ArrayList&lt;>();    expectedEvents.addAll(events);    expectedEvents.addAll(events);    assertEquals(expectedEvents, new ArrayList&lt;>(outputEvents), \"Unexpected events after expired ttl deduplication\"); }<\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0432\u0441\u0451 \u0442\u043e \u0436\u0435 \u0441\u0430\u043c\u043e\u0435, \u0447\u0442\u043e \u0438 \u0432 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u043c \u0442\u0435\u0441\u0442\u0435, \u043d\u043e \u043c\u044b \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c setStateTtlProcessingTime \u0432 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430 1 \u043c\u0441 \u0431\u043e\u043b\u044c\u0448\u0435, \u0447\u0435\u043c TTL. \u0422\u043e\u0433\u0434\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f, \u0447\u0442\u043e\u00a0 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f events \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u043b\u0438\u0441\u044c \u0440\u0430\u043d\u0435\u0435, \u0434\u043e\u043b\u0436\u043d\u0430 \u0443\u0434\u0430\u043b\u0438\u0442\u044c\u0441\u044f \u0438\u0437 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f ValueState&lt;Boolean>, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u041a\u0430\u043a \u0440\u0430\u0437 \u044d\u0442\u043e \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u0432 \u0431\u043b\u043e\u043a\u0435 \u0441 assert \u2014 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u043d\u0430 \u0432\u044b\u0445\u043e\u0434\u0435 \u0432 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430 \u0431\u043e\u043b\u044c\u0448\u0435, \u0442.\u00a0\u043a. \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b\u0438 \u0438\u0445 \u0434\u0432\u0430\u0436\u0434\u044b.<\/p>\n<p><a class=\"anchor\" name=\"7\" id=\"7\"><\/a><\/p>\n<h2>\u0412\u044b\u0432\u043e\u0434<\/h2>\n<p>\u041c\u044b \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0435\u043b\u0438, \u043a\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c stateless-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u044b \u0438 \u0431\u043e\u043b\u0435\u0435 \u0441\u043b\u043e\u0436\u043d\u044b\u0435 stateful-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u044b \u0441 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u043c \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0432\u0441\u043f\u043e\u043c\u043e\u0433\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0445 TestHarness-\u043a\u043b\u0430\u0441\u0441\u043e\u0432. \u0422\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u0438 \u043d\u0443\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 unit-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043d\u0430\u0448\u0435\u0433\u043e \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e, \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u043e\u0442 \u0432\u0441\u0435\u0445 \u0434\u0436\u043e\u0431. \u041d\u043e \u043d\u0430\u043c \u0435\u0449\u0451 \u043d\u0443\u0436\u043d\u0430 \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u044f, \u0447\u0442\u043e \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u044b \u0431\u0443\u0434\u0443\u0442 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c Flink-\u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0438 \u0447\u0442\u043e \u0432\u0435\u0441\u044c \u043f\u0430\u0439\u043f\u043b\u0430\u0439\u043d \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u044f \u043f\u043e\u043a\u0430\u0436\u0443, \u043a\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0435\u0441\u0442 \u043d\u0430 \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u0443\u044e \u0434\u0436\u043e\u0431\u0443 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c Flink MiniCluster, \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435. \u0410 \u0435\u0449\u0451 \u2014 \u043a\u0430\u043a \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u0445\u043e\u0434 \u043c\u0438\u043d\u0438-\u043a\u043b\u0430\u0441\u0442\u0435\u0440\u043e\u0432 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0445 stateful-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432.<\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/801693\/\"> https:\/\/habr.com\/ru\/articles\/801693\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041f\u0440\u0438\u0432\u0435\u0442 \u0432\u0441\u0435\u043c, \u043d\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441\u043d\u043e\u0432\u0430 \u0410\u043b\u0435\u043a\u0441\u0430\u043d\u0434\u0440 \u0411\u043e\u0431\u0440\u044f\u043a\u043e\u0432, \u0442\u0435\u0445\u043b\u0438\u0434 \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u041c\u0422\u0421 \u0410\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0438. \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u043c \u0446\u0438\u043a\u043b \u0441\u0442\u0430\u0442\u0435\u0439 \u043f\u0440\u043e \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a Apache Flink.<\/p>\n<p>\u041d\u0430\u043f\u043e\u043c\u043d\u044e, \u0432 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u0447\u0430\u0441\u0442\u044f\u0445 \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u043b \u043f\u0440\u043e \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0435 \u043f\u0430\u0439\u043f\u043b\u0430\u0439\u043d\u0430 Kafka-to-Kafka \u0441 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043e\u0447\u043d\u044b\u043c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u043f\u043e\u0442\u043e\u043a\u0430 \u0438 \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0446\u0438\u0435\u0439 \u0441\u043e\u0431\u044b\u0442\u0438\u0439. \u0422\u0430\u043a\u0436\u0435 \u0432 <a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/798667\/\">\u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435<\/a> \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u0430\u043b, \u043a\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u0434\u0438\u043d\u0430\u043c\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0432\u044b\u0445\u043e\u0434\u043d\u043e\u0439 Kafka-\u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u043e\u0433\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f.<\/p>\n<p>\u041d\u0430\u0447\u0438\u043d\u0430\u044f \u0441 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0438 \u043d\u0430\u0447\u043d\u0451\u043c \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0442\u044c, \u043a\u0430\u043a \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0441\u0451 \u043d\u0430\u0448\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Flink + Spring. \u041c\u043d\u043e\u0433\u0438\u0435 \u043e\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u0434\u0445\u043e\u0434\u044b \u0432\u043f\u043e\u043b\u043d\u0435 \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u043c\u044b \u0438 \u0432 \u043b\u044e\u0431\u043e\u043c \u0434\u0440\u0443\u0433\u043e\u043c \u043e\u0431\u044b\u0447\u043d\u043e\u043c Spring-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438, \u043f\u043e\u044d\u0442\u043e\u043c\u0443, \u043d\u0430\u0434\u0435\u044e\u0441\u044c, \u0432\u044b \u043d\u0430\u0439\u0434\u0451\u0442\u0435 \u0434\u043b\u044f \u0441\u0435\u0431\u044f \u0447\u0442\u043e-\u0442\u043e \u043d\u043e\u0432\u043e\u0435.<\/p>\n<p>\u0412 \u0434\u0430\u043d\u043d\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u043c\u044b \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c, \u043a\u0430\u043a \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c stateless- \u0438 stateful-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u044b Flink \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u0439 TestHarness.<\/p>\n<figure class=\"full-width\"><\/figure>\n<details class=\"spoiler\">\n<summary>\u0421\u043f\u0438\u0441\u043e\u043a \u043c\u043e\u0438\u0445 \u043f\u043e\u0441\u0442\u043e\u0432 \u043f\u0440\u043e Flink<\/summary>\n<div class=\"spoiler__content\">\n<ol>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/772898\/\">\u0412\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0432 Apache Flink: \u043e\u0441\u0432\u0430\u0438\u0432\u0430\u0435\u043c \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0445 \u043f\u0440\u0438\u043c\u0435\u0440\u0430\u0445<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/775970\/\">\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043f\u043e\u0434 Apache Flink: \u0441 \u0447\u0435\u0433\u043e \u043d\u0430\u0447\u0430\u0442\u044c?<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/786012\/\">Apache Flink. \u041a\u0430\u043a \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0446\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 \u043f\u043e\u0442\u043e\u043a\u0435 Kafka-to-Kafka?<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/798667\/\">\u0414\u0438\u043d\u0430\u043c\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0432\u044b\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0442\u043e\u043f\u0438\u043a\u0430<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/801693\/\"><strong>\u041a\u0430\u043a \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 unit-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432: Test Harness<\/strong><\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/812905\/\">Unit-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432, Job: Flink MiniCluster<\/a><\/p>\n<\/li>\n<li>\n<p>TBD (\u0432 \u043e\u0447\u0435\u0440\u0435\u0434\u0438)<\/p>\n<\/li>\n<\/ol>\n<\/div>\n<\/details>\n<p>\u0412\u0435\u0441\u044c \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u043c\u044b\u0439 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438 <a href=\"https:\/\/github.com\/AlexanderBobryakov\/flink-spring\">AlexanderBobryakov\/flink-spring<\/a>. \u0412 master-\u0432\u0435\u0442\u043a\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0438\u0442\u043e\u0433\u043e\u0432\u044b\u0439 \u043f\u0440\u043e\u0435\u043a\u0442 \u043f\u043e \u0432\u0441\u0435\u0439 \u0441\u0435\u0440\u0438\u0438 \u0441\u0442\u0430\u0442\u0435\u0439. \u0414\u0430\u043d\u043d\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0440\u0435\u043b\u0438\u0437\u043d\u043e\u0439 \u0432\u0435\u0442\u043a\u0435 \u0441 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c <a href=\"https:\/\/github.com\/AlexanderBobryakov\/flink-spring\/tree\/release\/4_testharness_deduplicator_test\">release\/4_testharness_deduplicator_test<\/a>.<\/p>\n<details class=\"spoiler\">\n<summary>\u041e\u0433\u043b\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0442\u044c\u0438<\/summary>\n<div class=\"spoiler__content\">\n<ol>\n<li>\n<p><a href=\"#1\">\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0442\u0435\u0441\u0442\u043e\u0432<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#2\">\u0410\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u044f EntityTestBuilder<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#3\">\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 stateless Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#4\">\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 stateful Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#5\">\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e TestHarness-\u043a\u043b\u0430\u0441\u0441\u043e\u0432<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#6\">\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e TestHarness-\u043a\u043b\u0430\u0441\u0441\u043e\u0432<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#7\">\u0412\u044b\u0432\u043e\u0434<\/a><\/p>\n<\/li>\n<\/ol>\n<\/div>\n<\/details>\n<p><a class=\"anchor\" name=\"1\" id=\"1\"><\/a><\/p>\n<h2>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0442\u0435\u0441\u0442\u043e\u0432<\/h2>\n<p>\u041d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u0441\u043f\u043e\u0439\u043b\u0435\u0440: \u043a \u043a\u043e\u043d\u0446\u0443 \u0441\u0442\u0430\u0442\u044c\u0438 \u043c\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u043c \u0432\u043e\u0442 \u0442\u0430\u043a\u0443\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u0442\u0435\u0441\u0442\u043e\u0432: <\/p>\n<figure class=\"full-width\"><\/figure>\n<p>\u041c\u044b \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u043f\u0435\u0440\u0432\u044b\u0435 \u0442\u0435\u0441\u0442\u044b \u043d\u0430 stateless-\u0444\u0438\u043b\u044c\u0442\u0440, stateful-\u0434\u0435\u0434\u0443\u043f\u043b\u0438\u043a\u0430\u0442\u043e\u0440 \u0438 \u043d\u0430 stateless-\u0441\u043f\u043b\u0438\u0442\u0435\u0440\u0430\u0442\u043e\u0440 (\u0443\u0447\u0438\u0442\u044b\u0432\u0430\u044e\u0449\u0438\u0439 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0435 Flink-\u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u0438).<\/p>\n<p><a class=\"anchor\" name=\"2\" id=\"2\"><\/a><\/p>\n<h2>\u0410\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u044f EntityTestBuilder<\/h2>\n<p>\u0412 \u043d\u0430\u0448\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0431\u0438\u0437\u043d\u0435\u0441-DTO, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 ClickMessage \u0438 ProductMessage, \u0438 \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043d\u043e\u0432\u044b\u0435. \u0412 \u0441\u0430\u043c\u0438\u0445 \u0442\u0435\u0441\u0442\u0430\u0445 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0445 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0435\u0432. \u041d\u043e \u043f\u043e\u0434\u0443\u043c\u0430\u0435\u043c \u043e\u0431 \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0435: \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043c \u0435\u0434\u0438\u043d\u043e\u0435 \u043c\u0435\u0441\u0442\u043e, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0431\u0443\u0434\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043a\u0430\u0436\u0434\u043e\u0435 DTO. \u0412\u0435\u0434\u044c \u0432 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u044f\u0445 \u043c\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u0435\u0439, \u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0433\u0443\u0442 \u0438\u043c\u0435\u0442\u044c \u0431\u0438\u0437\u043d\u0435\u0441-\u0446\u0435\u043d\u043d\u043e\u0441\u0442\u044c.<\/p>\n<p>\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0432 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441: <\/p>\n<pre><code class=\"java\">public interface EntityTestBuilder&lt;T> {     T build(); }<\/code><\/pre>\n<p>\u0414\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432 \u0443 \u043d\u0435\u0433\u043e \u0442\u0430\u043a\u0436\u0435 \u0431\u0443\u0434\u0443\u0442 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u043e\u0434 \u043a\u0430\u0436\u0434\u0443\u044e \u043d\u0430\u0448\u0443 DTO. \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043f\u0440\u0438\u043c\u0435\u0440 \u0434\u043b\u044f ClickMessage:<\/p>\n<pre><code class=\"java\">@With @AllArgsConstructor(access = PUBLIC) @NoArgsConstructor(access = PRIVATE) public class ClickMessageTestBuilder implements EntityTestBuilder&lt;ClickMessage> {    private UUID userId = UUID.randomUUID();    private String object = \"test_object\";    private Platform platform = Platform.Enum.APP;    private String productName = \"test_productName\";    private String productTopic = \"test_productTopic\";    private Long timestamp = 123L;    private Map&lt;String, Object> data = new HashMap&lt;>() {{        put(\"field_1\", \"value_1\");        put(\"field_2\", \"value_2\");    }};     public static ClickMessageTestBuilder aClickMessage() {        return new ClickMessageTestBuilder();    }     @Override    public ClickMessage build() {        return ClickMessage.builder()                   .userId(userId)                   .object(object)                   .platform(platform)                   .productName(productName)                   .productTopic(productTopic)                   .timestamp(timestamp)                   .data(data)                   .build();    } }<\/code><\/pre>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043c\u044b \u0432 \u043e\u0434\u043d\u043e\u043c \u043c\u0435\u0441\u0442\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u0432\u0441\u0435 \u0434\u0435\u0444\u043e\u043b\u0442\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0435\u0439, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u043c\u0435\u0442\u043e\u0434-\u0431\u0438\u043b\u0434\u0435\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u043c \u0432\u044b\u0437\u044b\u0432\u0430\u0442\u044c \u0432 \u0442\u0435\u0441\u0442\u0430\u0445 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u0430 ClickMessage. \u041f\u0440\u0438 \u044d\u0442\u043e\u043c \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0438 lombok \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0442\u044c \u0434\u0435\u0444\u043e\u043b\u0442\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0435\u0442\u043e\u0434\u043e\u0432 withXXX(&#8230;), \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440:<\/p>\n<pre><code class=\"java\">aClickMessage().withPlatform(APP).build()<\/code><\/pre>\n<p><a class=\"anchor\" name=\"3\" id=\"3\"><\/a><\/p>\n<h2>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 stateless Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432<\/h2>\n<p>\u041d\u0430\u0447\u043d\u0451\u043c \u0441 \u0441\u0430\u043c\u044b\u0445 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0442\u0435\u0441\u0442\u043e\u0432 \u043d\u0430 stateless-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u044b. \u041d\u0430\u043f\u043e\u043c\u043d\u044e, \u0447\u0442\u043e \u043e\u043d\u0438 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f. \u0418\u043c \u043d\u0435\u0432\u0430\u0436\u043d\u043e, \u043a\u0430\u043a\u0438\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043e\u043d\u0438 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043b\u0438 \u0440\u0430\u043d\u0435\u0435 \u2014 \u043a\u0430\u0436\u0434\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u043e\u0442 \u0434\u0440\u0443\u0433\u0438\u0445. \u0422\u0430\u043a\u043e\u0439 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043a\u0430\u043a \u043e\u0431\u044b\u0447\u043d\u044b\u0439 Java-\u043a\u043b\u0430\u0441\u0441, \u0438 \u0432 \u0435\u0433\u043e \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u043d\u0435\u0442 \u043d\u0438\u0447\u0435\u0433\u043e \u0441\u043b\u043e\u0436\u043d\u043e\u0433\u043e.<\/p>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430\u0448 \u0444\u0438\u043b\u044c\u0442\u0440, \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0449\u0438\u0439 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0441 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c \u043f\u043e\u043b\u044f platform:<\/p>\n<pre><code class=\"java\">public class ClickMessageWithPlatformFilter implements FilterFunction&lt;ClickMessage> {    private static final long serialVersionUID = 1L;     @Override    public boolean filter(ClickMessage message) {        return Optional.ofNullable(message.getPlatform())                   .map(platform -> WEB.equals(platform) || APP.equals(platform))                   .orElse(false);    } }<\/code><\/pre>\n<p>\u041f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e JUnit 5 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0438 @ParameterizedTest. \u042d\u0442\u0430 \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u0443 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0442 \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u0432\u0442\u043e\u0440\u043e\u0439 Source-\u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0438 (@MethodSource, @ValueSource, @CsvSource&#8230;):<\/p>\n<pre><code class=\"java\">class ClickMessageWithPlatformFilterUnitTest {     @ParameterizedTest    @MethodSource(\"clickMessagesByPlatform\")    void shouldFilterMessageByPlatform(boolean expectedFilter, ClickMessage message) {        final var filter = new ClickMessageWithPlatformFilter();        final var filterResult = filter.filter(message);        assertEquals(expectedFilter, filterResult, format(\"Unexpected filter result for message: %s\", message));    }     private static Stream&lt;Arguments> clickMessagesByPlatform() {        return Stream.of(            arguments(true, aClickMessage().withPlatform(APP).build()),            arguments(true, aClickMessage().withPlatform(WEB).build()),            arguments(false, aClickMessage().withPlatform(Platform.of(\"unknown\")).build()),            arguments(false, aClickMessage().withPlatform(Platform.of(\"\")).build()),            arguments(false, aClickMessage().withPlatform(null).build())        );    } }<\/code><\/pre>\n<ul>\n<li>\n<p>\u0432 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u043c \u043c\u0435\u0442\u043e\u0434\u0435 clickMessagesByPlatform \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u043f\u0435\u0440\u0435\u0447\u0435\u043d\u044c \u0432\u0441\u0435\u0445 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0439 \u0432\u0445\u043e\u0434\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0430 .boolean expectedFilter \u2014 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0432\u044b\u0437\u043e\u0432\u0430 \u0444\u0438\u043b\u044c\u0442\u0440\u0430 true\/false<\/p>\n<\/li>\n<li>\n<p>ClickMessage message \u2014 \u0441\u0430\u043c\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438 (\u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u044d\u0442\u043e\u0433\u043e \u043a\u043b\u0430\u0441\u0441\u0430)<\/p>\n<\/li>\n<\/ul>\n<p>\u0422\u0435\u0441\u0442 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0442\u044c\u0441\u044f 5 \u0440\u0430\u0437. \u0412 \u0441\u0430\u043c\u043e\u043c \u0442\u0435\u0441\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0451\u043c \u043e\u0431\u044a\u0435\u043a\u0442 \u0444\u0438\u043b\u044c\u0442\u0440\u0430 \u0438 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u043c \u043c\u0435\u0442\u043e\u0434 filter \u0434\u043b\u044f \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0435\u0433\u043e \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f \u0441 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u043c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u043c.<\/p>\n<p>\u0422\u0430\u043a \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u0430\u0441\u0441\u044b \u0434\u043b\u044f \u043c\u0435\u0442\u043e\u0434\u043e\u0432 map, flatMap, process \u0438 \u0442.\u00a0\u0434. \u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u0438\u043d\u043e\u0433\u0434\u0430 \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u044c\u0441\u044f \u0437\u0430\u043c\u043e\u043a\u0430\u0442\u044c \u043a\u0430\u043a\u043e\u0439-\u043d\u0438\u0431\u0443\u0434\u044c \u043e\u0431\u044a\u0435\u043a\u0442 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Mockito, \u043d\u043e \u044d\u0442\u043e \u043d\u0438\u0447\u0435\u043c \u043d\u0435 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f.<\/p>\n<p><a class=\"anchor\" name=\"4\" id=\"4\"><\/a><\/p>\n<h2>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 stateful Flink-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432<\/h2>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u0435\u0440\u0435\u0439\u0434\u0451\u043c \u043a \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044e stateful-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432. \u041e\u043d\u0438 \u0442\u0435\u0441\u043d\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u044b \u0441\u043e \u0441\u0440\u0435\u0434\u043e\u0439 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Flink, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043c\u043e\u0433\u0443\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435, \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u043e\u0435 Flink, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043a\u0430\u043a\u0438\u0435-\u043d\u0438\u0431\u0443\u0434\u044c \u0442\u0430\u0439\u043c\u0435\u0440\u044b \u0438 \u0442.\u00a0\u0434. \u0412 \u044d\u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043f\u043e\u0434\u0445\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u044f \u043e\u043f\u0438\u0441\u0430\u043b \u0432\u044b\u0448\u0435, \u043d\u0435 \u043f\u043e\u0434\u043e\u0439\u0434\u0451\u0442. \u041d\u0430 \u044d\u0442\u043e\u0442 \u0440\u0430\u0437 \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u044b \u0431\u043e\u043b\u0435\u0435 \u0443\u043c\u043d\u044b\u0435 \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0433\u0443\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f Flink. \u0422\u0430\u043a\u0438\u0435 \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u0438 \u0435\u0441\u0442\u044c \u0432 <a href=\"https:\/\/nightlies.apache.org\/flink\/flink-docs-release-1.17\/docs\/dev\/datastream\/testing\/#unit-testing-stateful-or-timely-udfs--custom-operators\">\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438<\/a> \u043a \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044e. <\/p>\n<p>\u0422\u0430\u043a\u0436\u0435 \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0430\u043a \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u043c\u044b\u0435 TestHarness-\u043a\u043b\u0430\u0441\u0441\u044b. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u043c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438:<\/p>\n<pre><code class=\"yaml\">testImplementation \"org.apache.flink:flink-streaming-java:${flinkVersion}:tests\" testImplementation \"org.apache.flink:flink-test-utils:${flinkVersion}\"<\/code><\/pre>\n<p>\u041f\u0435\u0440\u0432\u0430\u044f \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c \u0434\u0430\u0451\u0442 \u043d\u0430\u043c \u0434\u043e\u0441\u0442\u0443\u043f \u043a\u043e \u0432\u0441\u0435\u043c \u043e\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u043c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 TestHarness-\u043a\u043b\u0430\u0441\u0441\u0430\u043c, \u0430 \u0432\u0442\u043e\u0440\u0430\u044f \u2014 \u043a\u043e \u043c\u043d\u043e\u0433\u0438\u043c \u0443\u0434\u043e\u0431\u043d\u044b\u043c \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u044f\u043c \u0432 \u0432\u0438\u0434\u0435 Flink MiniCluster, TestEnvironment \u0438 \u0442.\u00a0\u0434.<\/p>\n<p><a class=\"anchor\" name=\"5\" id=\"5\"><\/a><\/p>\n<h2>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e TestHarness-\u043a\u043b\u0430\u0441\u0441\u043e\u0432<\/h2>\n<p>TestHarness \u2014 \u044d\u0442\u043e \u043a\u043b\u0430\u0441\u0441\u044b \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u0432, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043d\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0439 \u043b\u043e\u0433\u0438\u043a\u0438 Flink: watermark, \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f, \u0442\u0430\u0439\u043c\u0435\u0440\u044b \u0438 \u0442.\u00a0\u0434.<\/p>\n<p>\u0421\u0435\u0439\u0447\u0430\u0441 \u043e\u0431\u044a\u044f\u0441\u043d\u044e \u043d\u0430 \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u043e\u0431\u044b\u0447\u043d\u043e\u0433\u043e stateless-\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0430. \u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0442\u0435\u0441\u0442 \u043d\u0430\u0434 \u043d\u0430\u0448\u0438\u043c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u043c \u0440\u0430\u043d\u0435\u0435 \u0441\u043f\u043b\u0438\u0442\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0432\u0445\u043e\u0434\u043d\u044b\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u043f\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u043c \u043f\u043e\u0431\u043e\u0447\u043d\u044b\u043c \u0432\u044b\u0445\u043e\u0434\u043d\u044b\u043c \u043f\u043e\u0442\u043e\u043a\u0430\u043c (\u043f\u043e OutputTag) \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043f\u0440\u0435\u0434\u0438\u043a\u0430\u0442\u043e\u0432:<\/p>\n<pre><code class=\"java\">@RequiredArgsConstructor public class StreamSpliterator&lt;I> extends ProcessFunction&lt;I, Object> {    private static final long serialVersionUID = 1L;     private final Map&lt;OutputTag&lt;I>, SerializablePredicate&lt;I>> predicatesByTags;    private final OutputTag&lt;I> defaultTag;     @Override    public void processElement(I value, ProcessFunction&lt;I, Object>.Context ctx, Collector&lt;Object> out) {        for (Map.Entry&lt;OutputTag&lt;I>, SerializablePredicate&lt;I>> entry : predicatesByTags.entrySet()) {            final var predicate = entry.getValue();            if (predicate.test(value)) {                final var tag = entry.getKey();                ctx.output(tag, value);                return;            }        }        ctx.output(defaultTag, value);    } }<\/code><\/pre>\n<p>\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0435 \u043e\u0431\u044a\u044f\u0441\u043d\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u043c\u043e\u0436\u043d\u043e <a href=\"https:\/\/habr.com\/ru\/companies\/ru_mts\/articles\/786012\/\">\u0433\u043b\u044f\u043d\u0443\u0442\u044c \u0442\u0443\u0442<\/a>. \u041e\u0441\u043d\u043e\u0432\u043d\u0430\u044f \u0441\u043b\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u2014 \u0437\u0430\u043f\u0438\u0441\u044c \u0432 \u043e\u0431\u044a\u0435\u043a\u0442 \u043a\u043b\u0430\u0441\u0441\u0430 ProcessFunction.&lt;I, Object>Context \u043f\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u043e\u043c\u0443 \u0442\u0435\u0433\u0443 \u0432 \u0441\u0442\u0440\u043e\u043a\u0435 ctx.output(tag, value). \u041c\u043e\u0436\u043d\u043e, \u043a\u043e\u043d\u0435\u0447\u043d\u043e, \u0437\u0430\u043c\u043e\u043a\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u043a\u043b\u0430\u0441\u0441 \u0438 \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 unit-\u0442\u0435\u0441\u0442, \u043d\u043e \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043d\u0435 \u043e\u0447\u0435\u043d\u044c \u0445\u043e\u0440\u043e\u0448\u043e. \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0441\u044f, \u0447\u0442\u043e \u0442\u0435\u0441\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0437\u043d\u0430\u0442\u044c \u043e \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0439 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0435\/\u043b\u043e\u0433\u0438\u043a\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u043e\u0433\u043e \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0430. \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0431\u043e\u043b\u0435\u0435 \u043a\u0440\u0430\u0441\u0438\u0432\u044b\u0439 \u0442\u0435\u0441\u0442 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0432\u0441\u043f\u043e\u043c\u043e\u0433\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0445 TestHarness-\u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u0439:<\/p>\n<pre><code class=\"java\">class StreamSpliteratorUnitTest {     @Test    void shouldSplitStreamByPredicates() throws Exception {        final var webTag = new OutputTag&lt;>(\"web_tag\", TypeInformation.of(String.class));        final var mobileTag = new OutputTag&lt;>(\"mobile_tag\", TypeInformation.of(String.class));        final var unknownTag = new OutputTag&lt;>(\"unknown_tag\", TypeInformation.of(String.class));        final var events = List.of(            new StreamRecord&lt;>(\"web\"),            new StreamRecord&lt;>(\"mobile\"),            new StreamRecord&lt;>(\"test\"),            new StreamRecord&lt;>(\"web\")        );        final var spliterator = new StreamSpliterator&lt;>(            Map.of(                webTag, \"web\"::equals,                mobileTag, \"mobile\"::equals            ),            unknownTag        );        @Cleanup final var testHarness = ProcessFunctionTestHarnesses.forProcessFunction(spliterator);         testHarness.processElements(events);<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-371341","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/371341","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=371341"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/371341\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=371341"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=371341"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=371341"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}