{"id":2518,"date":"2022-09-28T19:35:50","date_gmt":"2022-09-29T00:35:50","guid":{"rendered":"https:\/\/badecho.com\/?p=2518"},"modified":"2023-02-20T20:41:16","modified_gmt":"2023-02-21T01:41:16","slug":"unit-testing-monogame","status":"publish","type":"post","link":"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/","title":{"rendered":"Unit Testing MonoGame&#8217;s Content Pipeline"},"content":{"rendered":"\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-full is-resized\"><img loading=\"lazy\" src=\"https:\/\/badecho.com\/wp-content\/uploads\/2022\/09\/UnitTestMonoGame.png\" alt=\"Unit Testing MonoGame's Content Pipeline\" class=\"wp-image-2520\" width=\"856\" height=\"448\" srcset=\"https:\/\/badecho.com\/wp-content\/uploads\/2022\/09\/UnitTestMonoGame.png 856w, https:\/\/badecho.com\/wp-content\/uploads\/2022\/09\/UnitTestMonoGame-300x157.png 300w, https:\/\/badecho.com\/wp-content\/uploads\/2022\/09\/UnitTestMonoGame-768x402.png 768w, https:\/\/badecho.com\/wp-content\/uploads\/2022\/09\/UnitTestMonoGame-480x251.png 480w\" sizes=\"(max-width: 856px) 100vw, 856px\" \/><\/figure><\/div>\n\n\n\n<p>Previously, I <a href=\"https:\/\/badecho.com\/index.php\/2022\/08\/17\/extending-pipeline\/\" target=\"_blank\" rel=\"noreferrer noopener\">wrote an article that showed how to add support for custom game data<\/a> by extending MonoGame&#8217;s content pipeline. Writing a pipeline extension is one thing, but if we wish to be responsible and smart developers, it may make sense to also write some unit testing code to ensure everything works.<\/p>\n\n\n\n<p>That can prove challenging, as MonoGame&#8217;s various subsystems are laden with rather sticky dependencies that can be hard to satisfy within a barebones unit testing environment. <\/p>\n\n\n\n<p>This article seeks to spare you, the reader, some of the pain you&#8217;re likely to experience when writing unit tests for a pipeline extension. So, read on, and let&#8217;s get to testing.<\/p>\n\n\n\n<h2>Two Major Aspects to Test<\/h2>\n\n\n\n<p>From a high-level perspective, there are two separate pieces to the content pipeline testing puzzle (each with its own challenges and problems) that we need to solve.<\/p>\n\n\n\n<p>The first area of concern is <em>pipeline content importation<\/em>. It is the more straightforward of the two. Tests must ensure that assets are serialized correctly and that content types are handled properly by their respective importer and processor classes.<\/p>\n\n\n\n<p>The second area of concern is <em>pipeline content loading<\/em> via the <code>ContentManager<\/code> class. This is what produces the model class instances for our data, so it is pretty important. Unfortunately, <code>ContentManager<\/code> has dependencies that are hard to satisfy in a unit test.<\/p>\n\n\n\n<p>Hard, or at least unclear at first, but not impossible! Let&#8217;s see, then, how we can go about testing these two separate areas of concern easily.<\/p>\n\n\n\n<h2>Testing Pipeline Content Importation<\/h2>\n\n\n\n<p>Testing the code that writes game assets to the content pipeline is as simple as instantiating our various <code>ContentImporter<\/code> and <code>ContentProcessor<\/code> classes and calling their public methods of note.<\/p>\n\n\n\n<h6>Sample Content Importation Test Code<\/h6>\n\n\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\npublic class TileMapPipelineTests\n{\n    private readonly TileMapImporter _importer = new();\n    private readonly TileMapProcessor _processor = new();\n    private readonly TestContentImporterContext _importerContext = new();\n    private readonly TestContentProcessorContext _processorContext = new();\n\n    [Fact]\n    public void ImportProcess_Csv_ReturnsValid() \n        =&gt; ValidateTileMap(&quot;GrassCsvFormat.tmx&quot;);\n\n    [Fact]\n    public void ImportProcess_Zlib_ReturnsValid() \n        =&gt; ValidateTileMap(&quot;GrassZlibFormat.tmx&quot;);\n\n    [Fact]\n    public void ImportProcess_Gzip_ReturnsValid()\n        =&gt; ValidateTileMap(&quot;GrassGzipFormat.tmx&quot;);\n\n    [Fact]\n    public void ImportProcess_UncompressedBase64_ReturnsValid()\n        =&gt; ValidateTileMap(&quot;GrassUncompressedBase64Format.tmx&quot;);\n\n    private void ValidateTileMap(string assetName)\n    {\n        TileMapContent content = _importer.Import($&quot;Content\\\\Tiles\\\\{assetName}&quot;, _importerContext);\n\n        Assert.NotNull(content);\n        Assert.NotNull(content.Asset);\n        Assert.NotEmpty(content.Asset.Layers);\n\n        content = _processor.Process(content, _processorContext);\n\n        var tileLayer = content.Asset.Layers.First() as TileLayerAsset;\n\n        Assert.NotNull(tileLayer);\n        Assert.NotEmpty(tileLayer.Tiles);\n    }\n}\n<\/pre>\n\n\n<p>Derivations of <code>ContentImporter<\/code> and <code>ContentProcessor<\/code> types are easy to work with, as they&#8217;ll have default constructors and core routines that are easily invoked. A path to the game asset is pretty much all you need.<\/p>\n\n\n\n<h3>Content Contexts<\/h3>\n\n\n\n<p>The only pieces of required data that aren&#8217;t immediately and readily available are the <code>ContentImporterContext<\/code> and <code>ContentProcessorContext<\/code> instances required by the core routines of our importer and processor, respectively.<\/p>\n\n\n\n<p>These types are abstract classes, and while you can make use of the implementations provided by MonoGame&#8217;s libraries, it&#8217;s going to be a lot easier if you just provide your own stub implementations.<\/p>\n\n\n\n<p>Additionally, if you&#8217;re using some kind of third-party mocking framework, you can just use that instead. <\/p>\n\n\n\n<h6>Content Importer Context Stub<\/h6>\n\n\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n\/\/\/ &lt;summary&gt;\n\/\/\/ Provides a content processor context stub to use for unit testing content importers.\n\/\/\/ &lt;\/summary&gt;\ninternal sealed class TestContentImporterContext : ContentImporterContext\n{\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override void AddDependency(string filename)\n    { }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override string IntermediateDirectory\n        =&gt; &quot;Content&quot;;\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override ContentBuildLogger Logger\n        =&gt; new PipelineBuildLogger();\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override string OutputDirectory \n        =&gt; &quot;Content&quot;;\n}\n<\/pre>\n\n\n<h6>Content Processor Context Stub<\/h6>\n\n\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n\/\/\/ &lt;summary&gt;\n\/\/\/ Provides a content processor context stub to use for unit testing content processors.\n\/\/\/ &lt;\/summary&gt;\ninternal sealed class TestContentProcessorContext : ContentProcessorContext\n{\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override void AddDependency(string filename)\n    { }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override void AddOutputFile(string filename)\n    { }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override TOutput BuildAndLoadAsset&lt;TInput, TOutput&gt;(\n        ExternalReference&lt;TInput&gt; sourceAsset,\n        string processorName,\n        OpaqueDataDictionary processorParameters,\n        string importerName)\n    {\n        return (TOutput) FormatterServices.GetUninitializedObject(typeof(TOutput));\n    }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override ExternalReference&lt;TOutput&gt; BuildAsset&lt;TInput, TOutput&gt;(\n        ExternalReference&lt;TInput&gt; sourceAsset,\n        string processorName,\n        OpaqueDataDictionary processorParameters,\n        string importerName,\n        string assetName)\n    {\n        return new ExternalReference&lt;TOutput&gt;();\n    }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override TOutput Convert&lt;TInput, TOutput&gt;(\n        TInput input, string processorName, OpaqueDataDictionary processorParameters)\n    {\n        return (TOutput) FormatterServices.GetUninitializedObject(typeof(TOutput));\n    }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override string BuildConfiguration\n        =&gt; string.Empty;\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override string IntermediateDirectory\n        =&gt; string.Empty;\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override ContentBuildLogger Logger\n        =&gt; new PipelineBuildLogger();\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override ContentIdentity SourceIdentity\n        =&gt; new();\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override string OutputDirectory\n        =&gt; &quot;Content&quot;;\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override string OutputFilename\n        =&gt; string.Empty;\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override OpaqueDataDictionary Parameters\n        =&gt; new();\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override TargetPlatform TargetPlatform \n        =&gt; TargetPlatform.DesktopGL;\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public override GraphicsProfile TargetProfile\n        =&gt; GraphicsProfile.HiDef;\n}\n<\/pre>\n\n\n<p>The <code>PipelineLogger<\/code> class used above can be found in the <code>MonoGame.Framework.Content.Pipeline.Builder<\/code> namespace.<\/p>\n\n\n\n<h2>Testing Pipeline Content Loading<\/h2>\n\n\n\n<p>This area of concern is the more critical of the two, as it will be testing code &#8220;at the end of the road&#8221;, as far as the whole content pipeline process goes.<\/p>\n\n\n\n<p>These tests need to be able to load a previously encoded game asset into a respective model class instance so that we can validate its data.<\/p>\n\n\n\n<p>Loading content (encoded in an <code>.xnb<\/code> file) from the content pipeline requires the use of the <code>ContentManager<\/code> class. If you&#8217;ve worked with MonoGame at all, you&#8217;ve used this class; however, you&#8217;ve probably only done so with an actual <code>Game<\/code> module.<\/p>\n\n\n\n<p><code>ContentManager<\/code> has a bunch of dependencies that prevent it from being immediately and easily used from the confines of a unit test. In particular: it requires an initialized <em>graphics device<\/em>. Not good if we&#8217;re testing in a headless environment!<\/p>\n\n\n\n<h6>Sample Content Loading Test Code<\/h6>\n\n\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\npublic class TileMapTests : IClassFixture&lt;ContentManagerFixture&gt;\n{\n    private readonly Microsoft.Xna.Framework.Content.ContentManager _content;\n\n    public TileMapTests(ContentManagerFixture contentFixture) \n        =&gt; _content = contentFixture.Content;\n\n    [Theory]\n    [InlineData(&quot;GrassFourTiles&quot;)]\n    [InlineData(&quot;GrassLeftDownOrder&quot;)]\n    [InlineData(&quot;GrassLargeBlue&quot;)]\n    [InlineData(&quot;GrassTilesAndImage&quot;)]\n    [InlineData(&quot;GrassTwoTileLayers&quot;)]\n    public void Load_NotNull(string mapName)\n    {\n        TileMap map = _content.Load&lt;TileMap&gt;($&quot;Tiles\\\\{mapName}&quot;);\n        \n        Assert.NotNull(map);\n    }\n    \/\/ ...and all other unit tests.\n}\n<\/pre>\n\n\n<p>This is the ideal unit test: one which allows us to see what&#8217;s returned by <code>ContentManager<\/code>. The initialization of the <code>ContentManager<\/code> instance seen above is handled by our <code>ContentManagerFixture<\/code> xUnit-specific class, and we shall go over how it does this shortly.<\/p>\n\n\n\n<p>It&#8217;s not very difficult to initialize a <code>ContentManager<\/code> type, as the parameters are easily provided. While that&#8217;s great, what isn&#8217;t is the fact that it is most certainly going to crash and burn during runtime.<\/p>\n\n\n\n<p>Let&#8217;s go over how to avoid that.<\/p>\n\n\n\n<h3>The MonoGame.Framework.WindowsDX Framework Must Be Used<\/h3>\n\n\n\n<p>My game and game library code uses the <code>MonoGame.Framework.DesktopGL<\/code> assembly so the code can be all nice and platform-neutral. Attempting to use <code>ContentManager<\/code> from this particular library, however, will most certainly cause errors during runtime.<\/p>\n\n\n\n<p>The reasons for this are implementation-specific and (from what I&#8217;ve gathered) the content manager from <code>MonoGame.Framework.DesktopGL<\/code> simply <em>cannot<\/em> work without an actual and real graphics device being present.<\/p>\n\n\n\n<p>I was able to get some testing code to compile and run fine on my local machine using OpenGL via some tricks; however, unit tests would fail when run as GitHub actions since those are run in a headless environment with no graphics device present.<\/p>\n\n\n\n<h3>A Means to an End<\/h3>\n\n\n\n<p>The MonoGame DirectX framework is much more forgiving when it comes to &#8220;faked&#8221; graphics devices, so you have to use that. And, since we&#8217;re simply using it as a means to make unit testing content pipeline output possible, this really shouldn&#8217;t conflict with the design goals of most projects that are intended to be platform-neutral.<\/p>\n\n\n\n<p>That is unless the custom game asset type being added through your pipeline extension is affected significantly by the particular graphics API in use. If that&#8217;s the case: you might be out of luck. You&#8217;ll need to hack the <code>MonoGame.Framework.DesktopGL <\/code>that you&#8217;re using.<code> <\/code><\/p>\n\n\n\n<p>Otherwise, we can use the DirectX-targeting assembly&#8217;s <code>ContentManager<\/code> to test code that&#8217;s targeting OpenGL with no issue.<\/p>\n\n\n\n<h3>Our Fake Graphics Device Service<\/h3>\n\n\n\n<p>To create a <code>ContentManager<\/code> instance, we need to provide it with a <code>ServiceContainer<\/code> configured to honor requests for the <code>IGraphicsDeviceService<\/code> type. We need to provide an implementation of this interface.<\/p>\n\n\n\n<h6>GraphicsDeviceService.cs<\/h6>\n\n\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n\/\/\/ &lt;summary&gt;\n\/\/\/ Provides a service for graphics devices used in unit tests.\n\/\/\/ &lt;\/summary&gt;\ninternal sealed class GraphicsDeviceService : IGraphicsDeviceService\n{\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ Initializes a new instance of the &lt;see cref=&quot;GraphicsDeviceService&quot;\/&gt; class.\n    \/\/\/ &lt;\/summary&gt;\n    public GraphicsDeviceService()\n    {\n        GraphicsDevice = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,\n                                            GraphicsProfile.Reach,\n                                            new PresentationParameters());\n    }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public event EventHandler&lt;EventArgs&gt;? DeviceCreated\n    {\n        add { }\n        remove { }\n    }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public event EventHandler&lt;EventArgs&gt;? DeviceDisposing\n    {\n        add { }\n        remove { }\n    }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public event EventHandler&lt;EventArgs&gt;? DeviceReset\n    {\n        add { }\n        remove { }\n    }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public event EventHandler&lt;EventArgs&gt;? DeviceResetting\n    {\n        add { }\n        remove { }\n    }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public GraphicsDevice GraphicsDevice \n    { get; }\n}\n<\/pre>\n\n\n<p>Very simple. Much simpler than if we&#8217;re trying to work with the OpenGL library&#8217;s <code>ContentManager<\/code>, which won&#8217;t work anyway in headless environments.<\/p>\n\n\n\n<h3>The Content Manager Fixture<\/h3>\n\n\n\n<p>Let&#8217;s look at the class that actually initializes our content manager to be used by our various pipeline unit test classes.<\/p>\n\n\n\n<h6>ContentManagerFixture.cs<\/h6>\n\n\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n\/\/\/ &lt;summary&gt;\n\/\/\/ Provides a shared context between test instances requiring the use of a content manager.\n\/\/\/ &lt;\/summary&gt;\npublic sealed class ContentManagerFixture : IDisposable\n{\n    public ContentManagerFixture()\n    {\n        var services = new ServiceContainer();\n        var graphicsService = new GraphicsDeviceService();\n\n        services.AddService(typeof(IGraphicsDeviceService), graphicsService);\n\n        Content = new Microsoft.Xna.Framework.Content.ContentManager(services, &quot;Content&quot;);\n    }\n\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ Gets an initialized content manager to use for testing.\n    \/\/\/ &lt;\/summary&gt;\n    public Microsoft.Xna.Framework.Content.ContentManager Content\n    { get; }\n\n    \/\/\/ &lt;inheritdoc \/&gt;\n    public void Dispose()\n    {\n        Content.Dispose();\n    }\n}\n<\/pre>\n\n\n<p>And voil\u00e0! We can now write unit tests for our custom pipeline extension code.<\/p>\n\n\n\n<p>Hope this helped!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Previously, I wrote an article that showed how to add support for custom game data by extending MonoGame&#8217;s content pipeline. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[10],"tags":[41,42,74,75],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v14.9 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\r\n<title>Unit Testing MonoGame&#039;s Content Pipeline - omni&#039;s hackpad<\/title>\r\n<meta name=\"description\" content=\"Unit testing different aspects of MonoGame can be challenging. Read on to see how we can go about unit testing the content pipeline.\" \/>\r\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\r\n<link rel=\"canonical\" href=\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/\" \/>\r\n<meta property=\"og:locale\" content=\"en_US\" \/>\r\n<meta property=\"og:type\" content=\"article\" \/>\r\n<meta property=\"og:title\" content=\"Unit Testing MonoGame&#039;s Content Pipeline - omni&#039;s hackpad\" \/>\r\n<meta property=\"og:description\" content=\"Unit testing different aspects of MonoGame can be challenging. Read on to see how we can go about unit testing the content pipeline.\" \/>\r\n<meta property=\"og:url\" content=\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/\" \/>\r\n<meta property=\"og:site_name\" content=\"omni&#039;s hackpad\" \/>\r\n<meta property=\"article:published_time\" content=\"2022-09-29T00:35:50+00:00\" \/>\r\n<meta property=\"article:modified_time\" content=\"2023-02-21T01:41:16+00:00\" \/>\r\n<meta property=\"og:image\" content=\"https:\/\/badecho.com\/wp-content\/uploads\/2022\/09\/UnitTestMonoGame.png\" \/>\r\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\r\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebSite\",\"@id\":\"https:\/\/badecho.com\/#website\",\"url\":\"https:\/\/badecho.com\/\",\"name\":\"omni&#039;s hackpad\",\"description\":\"Game Code Disassembly. Omnified Modification. Madness.\",\"publisher\":{\"@id\":\"https:\/\/badecho.com\/#\/schema\/person\/3de67496328be7ae6e1f52faf582e9d2\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":\"https:\/\/badecho.com\/?s={search_term_string}\",\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"ImageObject\",\"@id\":\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/#primaryimage\",\"inLanguage\":\"en-US\",\"url\":\"https:\/\/badecho.com\/wp-content\/uploads\/2022\/09\/UnitTestMonoGame.png\",\"width\":856,\"height\":448,\"caption\":\"Unit Testing MonoGame's Content Pipeline\"},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/#webpage\",\"url\":\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/\",\"name\":\"Unit Testing MonoGame's Content Pipeline - omni&#039;s hackpad\",\"isPartOf\":{\"@id\":\"https:\/\/badecho.com\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/#primaryimage\"},\"datePublished\":\"2022-09-29T00:35:50+00:00\",\"dateModified\":\"2023-02-21T01:41:16+00:00\",\"description\":\"Unit testing different aspects of MonoGame can be challenging. Read on to see how we can go about unit testing the content pipeline.\",\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/\"]}]},{\"@type\":\"Article\",\"@id\":\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/#webpage\"},\"author\":{\"@id\":\"https:\/\/badecho.com\/#\/schema\/person\/3de67496328be7ae6e1f52faf582e9d2\"},\"headline\":\"Unit Testing MonoGame&#8217;s Content Pipeline\",\"datePublished\":\"2022-09-29T00:35:50+00:00\",\"dateModified\":\"2023-02-21T01:41:16+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/#webpage\"},\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/badecho.com\/#\/schema\/person\/3de67496328be7ae6e1f52faf582e9d2\"},\"image\":{\"@id\":\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/#primaryimage\"},\"keywords\":\".NET,C#,MonoGame,XNA\",\"articleSection\":\"General Dev\",\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/badecho.com\/index.php\/2022\/09\/28\/unit-testing-monogame\/#respond\"]}]},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\/\/badecho.com\/#\/schema\/person\/3de67496328be7ae6e1f52faf582e9d2\",\"name\":\"Matt Weber\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\/\/badecho.com\/#personlogo\",\"inLanguage\":\"en-US\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/7e345ac2708b3a41c7bd70a4a0440d41?s=96&d=mm&r=g\",\"caption\":\"Matt Weber\"},\"logo\":{\"@id\":\"https:\/\/badecho.com\/#personlogo\"}}]}<\/script>\r\n<!-- \/ Yoast SEO plugin. -->","_links":{"self":[{"href":"https:\/\/badecho.com\/index.php\/wp-json\/wp\/v2\/posts\/2518"}],"collection":[{"href":"https:\/\/badecho.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/badecho.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/badecho.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/badecho.com\/index.php\/wp-json\/wp\/v2\/comments?post=2518"}],"version-history":[{"count":7,"href":"https:\/\/badecho.com\/index.php\/wp-json\/wp\/v2\/posts\/2518\/revisions"}],"predecessor-version":[{"id":2526,"href":"https:\/\/badecho.com\/index.php\/wp-json\/wp\/v2\/posts\/2518\/revisions\/2526"}],"wp:attachment":[{"href":"https:\/\/badecho.com\/index.php\/wp-json\/wp\/v2\/media?parent=2518"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/badecho.com\/index.php\/wp-json\/wp\/v2\/categories?post=2518"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/badecho.com\/index.php\/wp-json\/wp\/v2\/tags?post=2518"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}