4

I have an F# library with lots of non-public stuff I want to test. Currently all code that is not part of the assembly's public API are marked internal (specifically, it's placed in modules that are marked internal). I use the InternalsVisibleToAttribute to make this code visible to my test assembly. However, in order to get the test assembly to compile, all tests that use internal types in their signature (most of them, since I use FsCheck to auto-generate test inputs) must also be marked internal (this needs to be applied to each function, because internal modules won't be discovered by xunit). Additionally, any types used exclusively for FsCheck generation (e.g. type ValidCustomer = ValidCustomer of Customer where Customer is my internal domain type) also need to be marked internal, and FsCheck seems to get stuck when creating internal types, so the tests don't run.

Is there any way to test internal F# code (from a separate test assembly) without having to mark all tests whose signature depends on internal types as internal? Right now I'm simply leaning towards not making anything internal at all in the original code, but ideally there's a way to have my clean-API cake and eat it too.

cmeeren
  • 3,890
  • 2
  • 20
  • 50
  • 1
    Why are your *domain* types internal? Unless you use the terminology vastly different than, say, [DDD](http://amzn.to/WBCwx7), the domain model is the most important part of your application. It's the reason the application exists in the first place... – Mark Seemann Oct 06 '17 at 20:13
  • @MarkSeemann perhaps *domain types* is the wrong term. In this particular case, it's a single project that reads data from a data warehouse database, aggregates data on sales etc., and exposes that data to a web API. The API gets mostly numeric data, but there's quite a few internal, well, domain types (customer, order, etc. - they are modelling the domain, even though they're not used outside of this project) that is used in producing the aggregate data. – cmeeren Oct 07 '17 at 15:03
  • OK, but what is the motivation for making these types internal? – Mark Seemann Oct 07 '17 at 15:35
  • The motivation is cleaning up the public API of the assembly, to avoid any confusion as to what's supposed to be used by other projects importing it. It's not that important in my particular case - it's a small project in a small company - but there are certain parts of the code that are only meant for assembly-internal use, and I can imagine hypothetical (but realistic) examples where it's not automatically as clear-cut what project A referencing project B is supposed (and not supposed) to be using from project B. – cmeeren Oct 07 '17 at 19:52
  • IME, there's always better ways to solve such issues. In OOD, they go under the term *encapsulation*, but you can do similar in FP. I can't tell you about explicit alternatives without knowing details, though... – Mark Seemann Oct 07 '17 at 20:15
  • Sounds like this is more relevant for SE. I can't share much of the actual code, but if you tell me what kind of information you need, perhaps I can post it over there. – cmeeren Oct 08 '17 at 07:51
  • I'll be happy to look at concrete examples, here, or on SE... – Mark Seemann Oct 08 '17 at 07:56

1 Answers1

2

I've found that the OO world will generally be very opposed to attempting to test anything internal/private directly.

In the functional world I've seen more of a tendency to just expose public functions not intended for public use so they can be tested. See this comment from Edward Kmett.

When I started writing Haskell I started rethinking the way I used to approach encapsulation and hiding.

...

In general I'm a big fan of exposing all of the salient details, constructors and all for my data types through some kind of .Internal module, even if I want encapsulation and safety in the rest of the API.

...

As a side-effect of that you can then use these newly exposed guts to do nice testing. =)

There's a lot more detail in the original comment, and there is a talk where he discusses this at length but I can't find it now.

You could also give the module a really ugly name like __INTERNAL__ to discourage its use.

Community
  • 1
  • 1
TheQuickBrownFox
  • 10,544
  • 1
  • 22
  • 35