Delphi style: How to structure data modules for unit-testable code?

后端 未结 4 990
余生分开走
余生分开走 2021-02-04 12:26

I am looking for some advice about structuring Delphi programs for maintainability. I\'ve come to Delphi programming after a couple of decades of mostly C/C++ though I first lea

相关标签:
4条回答
  • 2021-02-04 12:32

    I think you need (and in fact, most delphi database developers are going to need) a Mock Dataset (Query, table, etc etc) Component that you could use, and substitute them at module-init time, for your current ADO dataset objects to this mock dataset, for test purposes. Instead of forcing Interfaces into your design, which are one way to provide a substitution capability, consider the fact that by Liskov substitution principle, you should be able to (at test fixture setup time), inject into your data module, the set of mock-datasets that you want to use, and simply replace the ADO datasets that you are using, at test execution time, with some other functionally equivalent entity (a mock dataset, or a file-backed table dataset).

    Perhaps you could even remove the datasets completely from the data module, and have them hooked up at runtime (in your main application) to the correct ADO dataset objects, and in unit tests, attach your mock datasets.

    Since you did not write the ADO dataset, you don't need to unit test it. However, mocking up such a dataset might be difficult.

    I would suggest you consider using a JvCsvDataSet or a ClientDataSet as the basis for your fixture (mock) datasets. You would be able to then use these to make sure that all your database platform dependencies (stuff that writes remote procedures or database SQL) is abstracted out into other classes, which again you are going to have to mock up. Such an effort might not only be required to make your business logic unit testable, it might also be a step towards becoming multiple-database-platform friendly in your business logic.

    imagine you have an ADOQuery called CustomerQuery, rename the object that you dropped onto your data module, to CustomerQueryImpl, and add this to your data module class declaration:

      private
            FCustomerQuery:TADOQuery;
    
      published
            property CustomerQuery:TADOQuery read FCustomerQuery write FCustomerQuery;
    

    then in your data module on create event, hook up the property to the objects:

       FCustomerQuery := CustomerQueryImpl
    

    Now you can write unit tests, which will 'hook' in and replace CustomerQuery with its own test fixture (mock object) at runtime.

    0 讨论(0)
  • 2021-02-04 12:33

    Please read this article, its about Unit Testing and Mock Objects including the theory of mock objects, localizing UT and interfaces discovery.

    hope you enjoy it.

    0 讨论(0)
  • 2021-02-04 12:39

    Personally I'm not a fan of TDataModule. It does very little to encourage good OO design principles. If all it was used for was a convenient container for DB components that would be one thing but far too often it becomes a dumping ground for business logic that would be better off in a domain layer. When this happens it winds up becoming a god class and a dependency magnet.

    Add to this a bug (or maybe its a feature) that's continued to exist since at least Delphi 2 that causes a form's data aware controls to lose their data sources if those data sources are located in a unit that isn't opened before the form.

    My suggestion

    • Add a domain layer between your UI and your database
    • Push as much of your business logic into domain objects as possible.
    • Make your UI and your data persistence layers as shallow as possible by using design and architectural patterns to delegate decision making to the domain layer.

    If you're not familiar with it the technique is referred to as domain driven design. Its certainly not the only solution but its a good one. The basic premise is that the UI, business logic and database change at different rates and for different reasons. So make the business logic a model of the problem domain and keep it separated from the UI and database.

    How does this make my code more testable?

    By moving the business logic to its own layer you can test it without interference from from either the UI or the database. This doesn't mean your code will be inherently testable simply because you put it in its own layer. Making legacy code testable is a difficult task. Most legacy code is tightly coupled so you will spend a good deal of time pulling it apart into classes with clearly defined responsibilities.

    Is this the Delphi style?

    That depends on your perspective. Traditionally, most Delphi applications were created by developing the UI and the database in tandem. Drop a few db aware controls on the form designer. Add/update a table with fields to store the control's data. Sprinkle with a liberal amount of business logic using event handlers. Viola! You just baked an application. For very small applications this is a great time saver. But lets not kid ourselves, small applications tend to turn into big ones and this design becomes an unsustainable maintenance nightmare.

    This really isn't the fault of the language. You find the same quick/dirty/shortsighted designs from hundreds of VB, C# and Java shops. These kinds of applications are the result of novice developers that don't know any better (and experienced developers that should know better), an IDE that makes it so easy to do and pressure to get the job done quickly.

    There are those in the Delphi community (as there are in other communities) that have been advocating better design techniques for a long time.

    0 讨论(0)
  • 2021-02-04 12:45

    Firstly before you change anything you need some unit tests so you can ensure you don't break anything. I would attempt to write unit tests against the current GUI without changing anything. DUnit has support for GUI testing (along with traditional unit testing) and although it's a little clunky and can't handle modal dialogs it is functional.

    Next, since your forms don't use data aware controls I would approach this by introducing another layer of data modules, a service layer if you will, between the forms and the existing global data modules.

    For every form in your application I would create a corresponding new service layer data module. This may sound like a lot of data modules but they're very lightweight and you can consolidate them later if you want.

    You could use ordinary TObjects rather than TDataModules for the service layer if you liked however using data modules gives you the flexibility of being able to place non-visual components on them later, for example a TClientDataSet and TDataSource if you went down the data-aware controls route at a later date.

    Initially each service layer data module would merely act as a proxy for accessing the global data modules. Your goal at this point would be simply to remove the direct dependency of the forms on the global data modules.

    Once the forms only indirectly accessed the global data modules via the service layer data modules then I would start to move functionality from the forms into the service layer. With this functionality in the service layer data modules you will find it much easier to write unit tests for new and existing code.

    At this point you could also start consolidating the per-form service layer data modules. It will be much easier to consolidate them now after the logic extraction from the forms is complete than if you try do do it during that process.

    0 讨论(0)
提交回复
热议问题