{"id":22843535,"url":"https://github.com/craigwardman/testdatadefinitionframework","last_synced_at":"2025-07-03T07:10:00.480Z","repository":{"id":65608335,"uuid":"394334583","full_name":"craigwardman/TestDataDefinitionFramework","owner":"craigwardman","description":"C# framework to abstract test data definition from the backing store so that the same set of tests can be run in memory and against a real data resource.","archived":false,"fork":false,"pushed_at":"2024-02-05T13:28:45.000Z","size":192,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-10T14:09:20.543Z","etag":null,"topics":["specflow","testing"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/craigwardman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-08-09T15:02:27.000Z","updated_at":"2023-03-02T17:19:09.000Z","dependencies_parsed_at":"2024-12-13T02:15:39.362Z","dependency_job_id":"a36ea281-35a3-442e-835f-f70b2534dde9","html_url":"https://github.com/craigwardman/TestDataDefinitionFramework","commit_stats":{"total_commits":34,"total_committers":1,"mean_commits":34.0,"dds":0.0,"last_synced_commit":"11a8b68f344276306a2fa084ce74ef8bf1188858"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/craigwardman/TestDataDefinitionFramework","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/craigwardman%2FTestDataDefinitionFramework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/craigwardman%2FTestDataDefinitionFramework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/craigwardman%2FTestDataDefinitionFramework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/craigwardman%2FTestDataDefinitionFramework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/craigwardman","download_url":"https://codeload.github.com/craigwardman/TestDataDefinitionFramework/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/craigwardman%2FTestDataDefinitionFramework/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263279304,"owners_count":23441683,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["specflow","testing"],"created_at":"2024-12-13T02:15:33.645Z","updated_at":"2025-07-03T07:10:00.456Z","avatar_url":"https://github.com/craigwardman.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TestDataDefinitionFramework\nTestDataDefinitionFramework (or TDDF for short) is a library that abstracts the setup of test data from the implementation of data backing stores, so that the same test code can be run against both an in-memory/fake repository or a \"real\" repository using only pre-compiler conditionals to switch between the two.\n\nAn immediate design limitation is that this method will ONLY work when you are using \"dumb data stores\", so if you are running multi-table SQL queries or stored procedures, then this is not the library for you!\n\nHowever, if your interactions with your data layer are usually that of \"store this entity in this table\", \"query this entity from this table\" then read on!\n\n## The Use Case\nOrdinarily when writing integration tests (for example using SpecFlow) you will decide; \"is this going to run against an in-memory fake, or against the \"real\" repository\".\n\nThere are pros and cons to deciding on one: \"in-memory\" is fast and can run on the build server, but doesn't thoroughly test your data provider layer. Using \"real\" repositories more thoroughly tests your code layers as it proves the integration of your code with the chosen data storage engine, but is slower and requires connectivity to a running instance of your data storage engine of choice (e.g. a MongoDB or SQL instance).\n\nIdeally it's nice to have both options, but a lot times this leads to a duplication of the integration test code - in SpecFlow terms the \"step definitions\" can look very different when you want to \"setup\" a MongoDB than when you only want to setup an in-memory context and pass that to an interceptor/fake repository.\n\nThe idea of TDDF is that by setting your test data against the TestDataStore you can then use that same data in an in-memory repository as easily as enabling a \"real\" backing store and the TDDF plugins will take care of actually standing up the \"real\" data resource.\n\n## Getting Started\n* Clone the source code repository and build, or install packages via NuGet.\n* Add a reference to \"TestDataDefinitionFramework.Core\" into your \"integration tests\" project (for example your SpecFlow/NUnit project).\n* Choose which backing stores plugins you want to use (e.g. TestDataDefinitionFramework.MongoDB)\n* Wire-up the code:\n\n1) Configure and Initialize (do this before your tests run), e.g. in SpecFlow\n```csharp\n[BeforeTestRun]\npublic static async Task Initialize()\n{\n    var mongoBackingStore = new MongoBackingStore(\"ExampleSutDB\");\n    TestDataStore.AddRepository\u003cSummaryItem\u003e(cfg =\u003e\n    {\n        cfg.WithName(SummaryCollection.Name);\n#if UseRealProvider\n            cfg.WithBackingStore(mongoBackingStore);\n#endif\n    });\n\n    await TestDataStore.InitializeAllAsync();\n}\n```\n\n2) Use the TestDataStore for your setting up your test data, e.g. in SpecFlow\n```csharp\n[Given(@\"the summaries repository returns '(.*)'\")]\npublic void GivenTheSummariesRepositoryReturns(IReadOnlyList\u003cstring\u003e items)\n{\n    TestDataStore.Repository\u003cSummaryItem\u003e(SummaryCollection.Name).Items = items.Select(i =\u003e new SummaryItem {Name = i}).ToArray();\n}\n```\n\n3) Add your in-memory repository fakes (for when running in-memory mode):\n```csharp\npublic class WebTestFixture : WebApplicationFactory\u003cStartup\u003e\n{\n    protected override void ConfigureWebHost(IWebHostBuilder builder)\n    {\n        base.ConfigureWebHost(builder);\n\n        builder.UseEnvironment(\"Testing\");\n\n        builder.ConfigureTestServices(services =\u003e\n        {\n#if !UseRealProvider\n            services.AddTransient\u003cISummariesRepository, InMemorySummariesRepository\u003e();\n#endif\n        });\n    }\n}\n```\n\n4) Implement your in-memory repository:\n```csharp\npublic class InMemorySummariesRepository : ISummariesRepository\n{\n    public Task\u003cIReadOnlyList\u003cstring\u003e\u003e GetAllAsync()\n    {\n        var result = TestDataStore.Repository\u003cSummaryItem\u003e(SummaryCollection.Name)\n                            .Items?.Select(i =\u003e i.Name).ToArray()\n            ?? Array.Empty\u003cstring\u003e();\n\n        return Task.FromResult((IReadOnlyList\u003cstring\u003e) result);\n    }\n}\n```\n\n5) Commit the test data before calling the SUT (the best way to acheive this in SpecFlow is to trigger before the \"When\" block)\n```csharp\n[BeforeScenarioBlock]\npublic async Task Commit()\n{\n    if (_scenarioContext.CurrentScenarioBlock == ScenarioBlock.When)\n    {\n        await TestDataStore.CommitAllAsync();\n    }\n}\n```\n\nNow you can run your tests against an in-memory fake, or against a \"real\" repository by simply setting or unsetting a pre-compiler conditional called \"UseRealProvider\".\n\n## Notes\n* See the ExampleTests project for a working version of the above\n* On first run, when you haven't specified your own connection string, TDDF will spin up containers using Docker - so please ensure Docker is installed and be patient on the first run while images are downloaded.\n\n## MongoDB Plugin Notes\n* Provide your own connection string if you already have a MongoDB instance running (beware that collections will be dropped/re-created)\n* If you don't provide a connection string, then the code will attempt to spin up a MongoDB instance on port 27017 using Docker Desktop (so this must be installed and that port must be free if you rely on this feature)\n* The collections will be dropped and re-created on each commit, so please don't point this at a working MongoDB database!\n* Make sure your \"repository\" names in TDDF match up with the collection name you use in the \"real\" repository\n\n## SQL Plugin Notes\n* Provide your own connection string if you already have a SQL instance running (beware that tables will be dropped/re-created)\n* If you don't provide a connection string, then the code will attempt to spin up a SQL instance on port 1433 using Docker Desktop (so this must be installed and that port must be free if you rely on this feature)\n* The database will be created when using the Docker version\n* Tables will be dropped and re-created on each commit, so please don't point this at a working SQL database!\n* Make sure your \"repository\" names in TDDF match up with the table name you use in the \"real\" repository\n* All objects are created in the dbo namespace\n* When using the built-in Docker hosted SQL server, in order to point the SUT at the correct connection string, override your configuration object in the WebTestFixture, e.g.\n```csharp\nservices.AddTransient\u003cSqlDataStoreConfig\u003e();\nservices.AddTransient\u003cISqlDataStoreConfig\u003e(sp =\u003e\n{\n\tvar config = sp.GetRequiredService\u003cSqlDataStoreConfig\u003e();\n\tconfig.ConnectionString = TestDataStore.Repository\u003cSummaryDescription\u003e().Config.BackingStore?\n\t\t.ConnectionString ?? config.ConnectionString;\n\treturn config;\n});\n```\n\n## Redis Plugin Notes\n* The plugin uses \"StringSet\", so only supports SUTs that use StringGet to obtain data.\n* Since Redis is a Key/Value store, you are required to provide the methods for serializing items into \"string key\" and \"string value\"\n* The serialization method should match exactly how the SUT works, so it can deserialize the tests data successfully\n* If your type doesn't naturally have a \"key\", then you can wrap it with a tuple, e.g.\n```csharp\nvar redisBackingStore = new RedisBackingStore(\n    new KeyValueResolver()\n        .WithResolver\u003c(string Key, YourTypeHere Value)\u003e(\n            item =\u003e (item.Key, sutRedisSerializer.Serialize(item.Value))\n        ));\n        \nTestDataStore.AddRepository\u003c(string Key, YourTypeHere Value)\u003e(cfg =\u003e\n        {\n#if UseRealProvider\n            cfg.WithBackingStore(redisBackingStore);\n#endif\n        });\n```\n\n## Architecture\n![Architecture Diagram](/docs/Architecture.png)\n\n## Advanced Examples\n### Using a custom builder\nIf you want to build up an object across multiple steps and then \"build\" it as part of the commit, then hook in earlier than the TDDF commit to set the state in the TestDataStore rather than having to remove the builder, e.g.:\n\n```csharp\n[Binding]\npublic class Context\n{\n    private readonly ScenarioContext _scenarioContext;\n\n    public Context(ScenarioContext scenarioContext)\n    {\n        _scenarioContext = scenarioContext;\n    }\n\n    public MyClassDataBuilder MyClassDataBuilder { get; set; }\n\n    [BeforeScenarioBlock(Order = 0)]\n    public void BeforeCommit()\n    {\n        if (_scenarioContext.CurrentScenarioBlock == ScenarioBlock.When)\n        {\n            var myClassInstance = MyClassDataBuilder?.Build();\n            TestDataStore.Repository\u003cMyClass\u003e().Items =\n                myClassInstance != null ?\n                new[] { myClassInstance } :\n                Array.Empty\u003cMyClass\u003e();\n        }\n    }\n}\n```\n\n### Capturing provider calls with an interceptor when in \"real\" mode\nSometimes you'd like to capture calls that were made to your provider layer so that you can make assertions about what was called and with what data. Obviously by swapping out the interceptor to the \"real\" provider you lose this functionality (unless you could make the same assertion against the \"real\" repository, but that seems like a bigger problem).\n\nThe solution when using TDDF is *not* to remove your interceptor when switching to \"real\" mode, but instead to use the interceptor class as a decorator over the \"real\" implementation and inject the \"real\" class only when running in that mode. For example:\n\n```csharp\nprotected override void ConfigureWebHost(IWebHostBuilder builder)\n{\n    base.ConfigureWebHost(builder);\n\n    builder.UseEnvironment(\"Testing\");\n\n    builder.ConfigureTestServices(services =\u003e\n    {\n        services.AddTransient\u003cIMyDataStore, MyDataStoreInterceptor\u003e(); // \u003c-- always use the interceptor\n\n#if UseRealProvider\n        services.AddTransient\u003cRealMyDataStore\u003e(); // \u003c-- when \"real\" mode, register the real implementation with .net DI\n#endif\n    }\n}\n\npublic class MyDataStoreInterceptor : IMyDataStore\n{\n    private readonly InterceptorsDataContext _interceptorsDataContext;\n    private readonly RealMyDataStore _realDataStore;\n\n    public MyDataStoreInterceptor(InterceptorsDataContext interceptorsDataContext, RealMyDataStore realDataStore = null) // \u003c-- null when running in memory\n    {\n        _interceptorsDataContext = interceptorsDataContext;\n        _realDataStore = realDataStore;\n    }\n\n    public Task StoreAsync(MyClass data)\n    {\n        _interceptorsDataContext.MyDataStoreContext.StoredData = data;\n\n        return _realDataStore != null ? \n            _realDataStore.StoreAsync(data) : \n            Task.CompletedTask;\n    }\n\n    public Task\u003cMyClass\u003e GetAsync(string reference)\n    {\n        return _realDataStore != null ? \n            _realDataStore.GetAsync(reference) :\n            Task.FromResult(TestDataStore.Repository\u003cMyClass\u003e().Items?.FirstOrDefault(i =\u003e i.Reference == reference))\n    }\n}\n```\n\n## Contributing\nAs you can see this repository is still in it's infancy and so far I've only needed to create a few plugins.\nFeel free to create your own plugins and raise a merge request so this can grow in it's usefulness!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcraigwardman%2Ftestdatadefinitionframework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcraigwardman%2Ftestdatadefinitionframework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcraigwardman%2Ftestdatadefinitionframework/lists"}