{"id":1367,"date":"2024-10-23T15:17:14","date_gmt":"2024-10-23T15:17:14","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2024\/10\/23\/model-based-testing-with-testcontainers-and-jqwik\/"},"modified":"2024-10-23T15:17:14","modified_gmt":"2024-10-23T15:17:14","slug":"model-based-testing-with-testcontainers-and-jqwik","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2024\/10\/23\/model-based-testing-with-testcontainers-and-jqwik\/","title":{"rendered":"Model-Based Testing with Testcontainers and Jqwik"},"content":{"rendered":"<p>When testing complex systems, the more edge cases you can identify, the better your software performs in the real world. But how do you efficiently generate hundreds or thousands of meaningful tests that reveal hidden bugs? Enter model-based testing (MBT), a technique that automates test case generation by modeling your software\u2019s expected behavior.<\/p>\n<p>In this demo, we\u2019ll explore the model-based testing technique to perform regression testing on a simple REST API.<\/p>\n<p>We\u2019ll use the <a href=\"https:\/\/jqwik.net\/\" target=\"_blank\" rel=\"noopener\">jqwik<\/a> test engine on JUnit 5 to run property and model-based tests. Additionally, we\u2019ll use <a href=\"https:\/\/testcontainers.com\/getting-started\/\" target=\"_blank\" rel=\"noopener\">Testcontainers<\/a> to spin up Docker containers with different versions of our application.<\/p>\n<h2 class=\"wp-block-heading\"><strong>Model-based testing<\/strong><\/h2>\n<p>Model-based testing is a method for testing stateful software by comparing the tested component with a model that represents the expected behavior of the system. Instead of manually writing test cases, we\u2019ll use a testing tool that:<\/p>\n<p>Takes a list of possible actions supported by the application<\/p>\n<p>Automatically generates test sequences from these actions, targeting potential edge cases<\/p>\n<p>Executes these tests on the software and the model, comparing the results<\/p>\n<p>In our case, the actions are simply the endpoints exposed by the application\u2019s API. For the demo\u2019s code examples, we\u2019ll use a basic service with a CRUD REST API that allows us to:<\/p>\n<p>Find an employee by their unique employee number<\/p>\n<p>Update an employee\u2019s name<\/p>\n<p>Get a list of all the employees from a department<\/p>\n<p>Register a new employee<\/p>\n<p><button class=\"lightbox-trigger\"><\/button><\/p>\n<p>\t\tFigure 1: Finding an employee, updating their name, finding their department, and registering a new employee.<\/p>\n<p>Once everything is configured and we finally run the test, we can expect to see a rapid sequence of hundreds of requests being sent to the two stateful services:<\/p>\n<p><button class=\"lightbox-trigger\"><\/button><\/p>\n<p>\t\tFigure 2: New requests being sent to the two stateful services.<\/p>\n<h2 class=\"wp-block-heading\"><strong>Docker Compose<\/strong><\/h2>\n<p>Let\u2019s assume we need to switch the database from Postgres to MySQL and want to ensure the service\u2019s behavior remains consistent. To test this, we can run both versions of the application, send identical requests to each, and compare the responses.<\/p>\n<p>We can set up the environment using a Docker Compose that will run two versions of the app:<\/p>\n<p><strong>Model (mbt-demo:postgres)<\/strong>: The current live version and our source of truth.<\/p>\n<p><strong>Tested version (mbt-demo:mysql)<\/strong>: The new feature branch under test.<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nservices:<br \/>\n  ## MODEL<br \/>\n  app-model:<br \/>\n      image: mbt-demo:postgres<br \/>\n      # &#8230;<br \/>\n      depends_on:<br \/>\n          &#8211; postgres<br \/>\n  postgres:<br \/>\n      image: postgres:16-alpine<br \/>\n      # &#8230;\n<p>  ## TESTED<br \/>\n  app-tested:<br \/>\n    image: mbt-demo:mysql<br \/>\n    # &#8230;<br \/>\n    depends_on:<br \/>\n      &#8211; mysql<br \/>\n  mysql:<br \/>\n    image: mysql:8.0<br \/>\n    # &#8230;\n<\/p><\/div>\n<h2 class=\"wp-block-heading\"><strong>Testcontainers<\/strong><\/h2>\n<p>At this point, we could start the application and databases manually for testing, but this would be tedious. Instead, let\u2019s use Testcontainers\u2019 <a href=\"https:\/\/java.testcontainers.org\/modules\/docker_compose\/\" target=\"_blank\" rel=\"noopener\">ComposeContainer<\/a> to automate this with our Docker Compose file during the testing phase.<\/p>\n<p>In this example, we\u2019ll use jqwik as our JUnit 5 test runner. First, let\u2019s add the <a href=\"https:\/\/mvnrepository.com\/artifact\/net.jqwik\/jqwik\" target=\"_blank\" rel=\"noopener\">jqwik<\/a> and <a href=\"https:\/\/mvnrepository.com\/artifact\/org.testcontainers\/testcontainers\" target=\"_blank\" rel=\"noopener\">Testcontainers<\/a> and the <a href=\"https:\/\/mvnrepository.com\/artifact\/net.jqwik\/jqwik-testcontainers\" target=\"_blank\" rel=\"noopener\">jqwik-testcontainers<\/a> dependencies to our pom.xml:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\n&lt;dependency&gt;<br \/>\n    &lt;groupId&gt;net.jqwik&lt;\/groupId&gt;<br \/>\n    &lt;artifactId&gt;jqwik&lt;\/artifactId&gt;<br \/>\n    &lt;version&gt;1.9.0&lt;\/version&gt;<br \/>\n    &lt;scope&gt;test&lt;\/scope&gt;<br \/>\n&lt;\/dependency&gt;<br \/>\n&lt;dependency&gt;<br \/>\n    &lt;groupId&gt;net.jqwik&lt;\/groupId&gt;<br \/>\n    &lt;artifactId&gt;jqwik-testcontainers&lt;\/artifactId&gt;<br \/>\n    &lt;version&gt;0.5.2&lt;\/version&gt;<br \/>\n    &lt;scope&gt;test&lt;\/scope&gt;<br \/>\n&lt;\/dependency&gt;<br \/>\n&lt;dependency&gt;<br \/>\n    &lt;groupId&gt;org.testcontainers&lt;\/groupId&gt;<br \/>\n    &lt;artifactId&gt;testcontainers&lt;\/artifactId&gt;<br \/>\n    &lt;version&gt;1.20.1&lt;\/version&gt;<br \/>\n    &lt;scope&gt;test&lt;\/scope&gt;<br \/>\n&lt;\/dependency&gt;\n<\/div>\n<p>As a result, we can now instantiate a ComposeContainer and pass our test docker-compose file as argument:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\n@Testcontainers<br \/>\nclass ModelBasedTest {\n<p>    @Container<br \/>\n    static ComposeContainer ENV = new ComposeContainer(new File(&#8220;src\/test\/resources\/docker-compose-test.yml&#8221;))<br \/>\n       .withExposedService(&#8220;app-tested&#8221;, 8080, Wait.forHttp(&#8220;\/api\/employees&#8221;).forStatusCode(200))<br \/>\n       .withExposedService(&#8220;app-model&#8221;, 8080, Wait.forHttp(&#8220;\/api\/employees&#8221;).forStatusCode(200));<\/p>\n<p>    \/\/ tests<br \/>\n}\n<\/p><\/div>\n<h2 class=\"wp-block-heading\"><strong>Test HTTP client<\/strong><\/h2>\n<p>Now, let\u2019s create a small test utility that will help us execute the HTTP requests against our services:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nclass TestHttpClient {<br \/>\n  ApiResponse&lt;EmployeeDto&gt; get(String employeeNo) { \/* &#8230; *\/ }\n<p>  ApiResponse&lt;Void&gt; put(String employeeNo, String newName) { \/* &#8230; *\/ }<\/p>\n<p>  ApiResponse&lt;List&lt;EmployeeDto&gt;&gt; getByDepartment(String department) { \/* &#8230; *\/ }<\/p>\n<p>  ApiResponse&lt;EmployeeDto&gt; post(String employeeNo, String name) { \/* &#8230; *\/ }<\/p>\n<p>  record ApiResponse&lt;T&gt;(int statusCode, @Nullable T body) { }<\/p>\n<p>  record EmployeeDto(String employeeNo, String name) { }<br \/>\n}\n<\/p><\/div>\n<p>Additionally, in the test class, we can declare another method that helps us create TestHttpClients for the two services started by the ComposeContainer:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nstatic TestHttpClient testClient(String service) {<br \/>\n  int port = ENV.getServicePort(service, 8080);<br \/>\n  String url = &#8220;http:\/\/localhost:%s\/api\/employees&#8221;.formatted(port);<br \/>\n  return new TestHttpClient(service, url);<br \/>\n}\n<\/div>\n<h2 class=\"wp-block-heading\"><strong>jqwik<\/strong><\/h2>\n<p>Jqwik is a property-based testing framework for Java that integrates with JUnit 5, automatically generating test cases to validate properties of code across diverse inputs. By using generators to create varied and random test inputs, jqwik enhances test coverage and uncovers edge cases.<\/p>\n<p>If you\u2019re new to jqwik, you can explore their API in detail by reviewing the <a href=\"https:\/\/jqwik.net\/docs\/current\/user-guide.html\" target=\"_blank\" rel=\"noopener\">official user guide<\/a>. While this tutorial won\u2019t cover all the specifics of the API, it\u2019s essential to know that jqwik allows us to define a set of actions we want to test.<\/p>\n<p>To begin with, we\u2019ll use jqwik\u2019s @Property annotation \u2014 instead of the traditional @Test \u2014 to define a test:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\n@Property<br \/>\nvoid regressionTest() {<br \/>\n  TestHttpClient model = testClient(&#8220;app-model&#8221;);<br \/>\n  TestHttpClient tested = testClient(&#8220;app-tested&#8221;);<br \/>\n  \/\/ &#8230;<br \/>\n}\n<\/div>\n<p>Next, we\u2019ll define the actions, which are the HTTP calls to our APIs and can also include assertions.<\/p>\n<p>For instance, the GetOneEmployeeAction will try to fetch a specific employee from both services and compare the responses:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nrecord ModelVsTested(TestHttpClient model, TestHttpClient tested) {}\n<p>record GetOneEmployeeAction(String empNo) implements Action&lt;ModelVsTested&gt; {<br \/>\n  @Override<br \/>\n  public ModelVsTested run(ModelVsTested apps) {<br \/>\n    ApiResponse&lt;EmployeeDto&gt; actual = apps.tested.get(empNo);<br \/>\n    ApiResponse&lt;EmployeeDto&gt; expected = apps.model.get(empNo);<\/p>\n<p>    assertThat(actual)<br \/>\n      .satisfies(hasStatusCode(expected.statusCode()))<br \/>\n      .satisfies(hasBody(expected.body()));<br \/>\n    return apps;<br \/>\n  }<br \/>\n}\n<\/p><\/div>\n<p>Additionally, we\u2019ll need to wrap these actions within Arbitrary objects. We can think of Arbitraries as objects implementing the factory design pattern that can generate a wide variety of instances of a type, based on a set of configured rules.<\/p>\n<p>For instance, the Arbitrary returned by employeeNos() can generate employee numbers by choosing a random department from the configured list and concatenating a number between 0 and 200:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nstatic Arbitrary&lt;String&gt; employeeNos() {<br \/>\n  Arbitrary&lt;String&gt; departments = Arbitraries.of(&#8220;Frontend&#8221;, &#8220;Backend&#8221;, &#8220;HR&#8221;, &#8220;Creative&#8221;, &#8220;DevOps&#8221;);<br \/>\n  Arbitrary&lt;Long&gt; ids = Arbitraries.longs().between(1, 200);<br \/>\n  return Combinators.combine(departments, ids).as(&#8220;%s-%s&#8221;::formatted);<br \/>\n}\n<\/div>\n<p>Similarly, getOneEmployeeAction() returns an Aribtrary action based on a given Arbitrary employee number:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nstatic Arbitrary&lt;GetOneEmployeeAction&gt; getOneEmployeeAction() {<br \/>\n  return employeeNos().map(GetOneEmployeeAction::new);<br \/>\n}\n<\/div>\n<p>After declaring all the other Actions and Arbitraries, we\u2019ll create an ActionSequence:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\n@Provide<br \/>\nArbitrary&lt;ActionSequence&lt;ModelVsTested&gt;&gt; mbtJqwikActions() {<br \/>\n  return Arbitraries.sequences(<br \/>\n    Arbitraries.oneOf(<br \/>\n      MbtJqwikActions.getOneEmployeeAction(),<br \/>\n      MbtJqwikActions.getEmployeesByDepartmentAction(),<br \/>\n      MbtJqwikActions.createEmployeeAction(),<br \/>\n      MbtJqwikActions.updateEmployeeNameAction()<br \/>\n  ));<br \/>\n}\n<p>static Arbitrary&lt;Action&lt;ModelVsTested&gt;&gt; getOneEmployeeAction() { \/* &#8230; *\/ }<br \/>\nstatic Arbitrary&lt;Action&lt;ModelVsTested&gt;&gt; getEmployeesByDepartmentAction() { \/* &#8230; *\/ }<br \/>\n\/\/ same for the other actions\n<\/p><\/div>\n<p>Now, we can write our test and leverage jqwik to use the provided actions to test various sequences. Let\u2019s create the ModelVsTested tuple and use it to execute the sequence of actions against it:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\n@Property<br \/>\nvoid regressionTest(@ForAll(&#8220;mbtJqwikActions&#8221;) ActionSequence&lt;ModelVsTested&gt; actions) {<br \/>\n  ModelVsTested testVsModel = new ModelVsTested(<br \/>\n    testClient(&#8220;app-model&#8221;),<br \/>\n    testClient(&#8220;app-tested&#8221;)<br \/>\n  );<br \/>\n  actions.run(testVsModel);<br \/>\n}\n<\/div>\n<p>That\u2019s it \u2014 we can finally run the test! The test will generate a sequence of thousands of requests trying to find inconsistencies between the model and the tested service:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-tested] PUT \/api\/employeesFrontend-129?name=v<br \/>\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-model] PUT \/api\/employeesFrontend-129?name=v<br \/>\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-tested] GET \/api\/employees\/Frontend-129<br \/>\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-model] GET \/api\/employees\/Frontend-129<br \/>\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-tested] POST \/api\/employees { name=sdxToS, empNo=Frontend-91 }<br \/>\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-model] POST \/api\/employees { name=sdxToS, empNo=Frontend-91 }<br \/>\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-tested] PUT \/api\/employeesFrontend-4?name=PZbmodNLNwX<br \/>\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-model] PUT \/api\/employeesFrontend-4?name=PZbmodNLNwX<br \/>\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-tested] GET \/api\/employees\/Frontend-4<br \/>\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-model] GET \/api\/employees\/Frontend-4<br \/>\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-tested] GET \/api\/employees?department=\u067a\u2bdf\u6878<br \/>\nINFO com.etr.demo.utils.TestHttpClient &#8212; [app-model] GET \/api\/employees?department=\u067a\u2bdf\u6878<br \/>\n        &#8230;\n<\/div>\n<h2 class=\"wp-block-heading\"><strong>Catching errors<\/strong><\/h2>\n<p>If we run the test and check the logs, we\u2019ll quickly spot a failure. It appears that when searching for employees by department with the argument \u067a\u2bdf\u6878 the model produces an internal server error, while the test version returns 200 OK:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nOriginal Sample<br \/>\n&#8212;&#8212;&#8212;&#8212;&#8212;<br \/>\nactions:<br \/>\nActionSequence[FAILED]: 8 actions run [<br \/>\n    UpdateEmployeeAction[empNo=Creative-13, newName=uRhplM],<br \/>\n    CreateEmployeeAction[empNo=Backend-184, name=aGAYQ],<br \/>\n    UpdateEmployeeAction[empNo=Backend-3, newName=aWCxzg],<br \/>\n    UpdateEmployeeAction[empNo=Frontend-93, newName=SrJTVwMvpy],<br \/>\n    UpdateEmployeeAction[empNo=Frontend-129, newName=v],<br \/>\n    CreateEmployeeAction[empNo=Frontend-91, name=sdxToS],<br \/>\n    UpdateEmployeeAction[empNo=Frontend-4, newName=PZbmodNLNwX],<br \/>\n    GetEmployeesByDepartmentAction[department=\u067a\u2bdf\u6878]<br \/>\n]<br \/>\n    final currentModel: ModelVsTested[model=com.etr.demo.utils.TestHttpClient@5dc0ff7d, tested=com.etr.demo.utils.TestHttpClient@64920dc2]<br \/>\nMultiple Failures (1 failure)<br \/>\n    &#8212; failure 1 &#8212;<br \/>\n    expected: 200<br \/>\n    but was: 500\n<\/div>\n<p>Upon investigation, we find that the issue arises from a native SQL query using Postgres-specific syntax to retrieve data. While this was a simple issue in our small application, model-based testing can help uncover unexpected behavior that may only surface after a specific sequence of repetitive steps pushes the system into a particular state.<\/p>\n<h1 class=\"wp-block-heading\">Wrap up<\/h1>\n<p>In this post, we provided hands-on examples of how model-based testing works in practice. From defining models to generating test cases, we\u2019ve seen a powerful approach to improving test coverage and reducing manual effort. Now that you\u2019ve seen the potential of model-based testing to enhance software quality, it\u2019s time to dive deeper and tailor it to your own projects.<\/p>\n<p><a href=\"https:\/\/github.com\/etrandafir93\/model-based-testing-practice\" target=\"_blank\" rel=\"noopener\">Clone the repository<\/a> to experiment further, customize the models, and integrate this methodology into your testing strategy. Start building more resilient software today!<\/p>\n<p><em>Thank you to <\/em><a href=\"https:\/\/github.com\/etrandafir93\" target=\"_blank\" rel=\"noopener\"><em>Emanuel Trandafir<\/em><\/a><em> for contributing this post.<\/em><\/p>\n<h2 class=\"wp-block-heading\">Learn more<\/h2>\n<p>Clone the <a href=\"https:\/\/github.com\/etrandafir93\/model-based-testing-practice\" target=\"_blank\" rel=\"noopener\">model-based testing practice repo<\/a>.<\/p>\n<p>Subscribe to the <a href=\"https:\/\/www.docker.com\/newsletter-subscription\/\" target=\"_blank\" rel=\"noopener\">Docker Newsletter<\/a>.\u00a0<\/p>\n<p>Visit the <a href=\"https:\/\/testcontainers.com\/\" target=\"_blank\" rel=\"noopener\">Testcontainers website<\/a>.<\/p>\n<p>Get started with Testcontainers Cloud by <a href=\"https:\/\/testcontainers.com\/cloud\" target=\"_blank\" rel=\"noopener\">creating a free account<\/a>.<\/p>\n<p>Have questions? The <a href=\"https:\/\/www.docker.com\/community\/\" target=\"_blank\" rel=\"noopener\">Docker community is here to help<\/a>.<\/p>\n<p>New to Docker? <a href=\"https:\/\/docs.docker.com\/desktop\/\" target=\"_blank\" rel=\"noopener\">Get started<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>When testing complex systems, the more edge cases you can identify, the better your software performs in the real world. [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[4],"tags":[],"class_list":["post-1367","post","type-post","status-publish","format-standard","hentry","category-docker"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/1367","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=1367"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/1367\/revisions"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=1367"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=1367"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=1367"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}