How to test functions that operate on abstract types

I have a function f(x::Vector). It’s tested on Vectors and seems to do okay. I looked at the code and think it should also work for all AbstractVectors, but I don’t know how to test this. In general, are there any principles for writing tests for abstract types? More particularly in the case of AbstractVectors and AbstractArrays, is there a list of common mistakes (e.g. 1:length(x) indexing) compiled somewhere? And most particularly, this is the function I’m trying to test.

You probably intend a function f(x::AbstractVector) as a Vector is not the same thing as an AbstractVector. (oh, I see that is how you have it in the source code linked).

There are many AbstractVector types e.g. subtypes(AbstractVector).
You are likely interested in testing over some set of element types T, like subtypes(Signed), subtypes(AbstractFloat), or maybe general Real or Number types. On the other hand, you may also want to know your function works with vectors of Char and vectors of String – or not. What is your intent?

Yes. I would like to broaden the function definition from f(x::Vector) to f(x::AbstractVector).

All you need to do in Julia is just specify the type (here the set of types) as AbstractVector and your function will be called with any legit realization of an AbstractVector. The important part is how to handle such a wide variety of possible inputs in an effective and reasonably concise way.

There some things that make sense for any realization of an AbstractVector, for example, finding its length and determining if the vector is empty. Julia already has length and isempty that work with AbstractVectors. We can confirm this using the builtin hasmethod. To make its use simpler, define

Base.hasmethod(method, ::Type{T}) where {T} = hasmethod(method, (T,))

now

julia> hasmethod(length, AbstractVector)
true
julia> hasmethod(isempty, AbstractVector)
true

You are free to use any of the methods that are predefined for AbstractVectors in the design of your tests.

Really the only things to consider is OffsetArray and StaticVector (if applicable). If those work, pretty much everything else should too.

1 Like

That’s a nice, easy, answer! I’ll test on those two. What makes you think that those two cover everything?

In this case, I am interested in exploring exotic container subtypes rather than element types. I already plan to narrow the element types to the concrete set Union{Float32, Missing}, Union{Float64, Missing}, Float32, Float64, Missing

This:

https://github.com/JuliaLang/julia/blob/898142d5b59ab75b586d1fbd4d48eec37b004f40/base/sort.jl#L1220

Won’t work for StaticArrays.

Wow! I never would have guessed! Why not?

You cannot overwrite static values.

They are not mutable, you cannot change each component independently. Actually you can’t have a mutating function (indicated there by the !) written for StaticArrays. The “best” you can do is to redefine them and return the new ones.

So, as a rule of thumb, none of the mutating functions are expected to work on static arrays (although they may provide that possiblity if they return the array as well, depending on what the function does).

Offset arrays cover non-1 based indexing, and static arrays covers the case where you can’t modify the vector. Other than that, the array interface is relatively straightforward. If there’s a bug in something, it would probably be a bug in the array type.

Thanks! I forgot that some of them are immutable in addition to being statically sized. I expect that in this case it is okay to declare a function f!(x::AbstractArray) and throw a setindex!(...) not defined error whenever someone calls f!(x::SomeImmutibleSubtypeOfAbstractArray)?

sure – although that is not strictly necessary … if you don’t , Julia will

julia> using StaticArrays
julia> staticvec = SVector{4}([1,2,3,4])
julia> staticvec[1] = 100
ERROR: setindex!(::SVector{4, Int64}, value, ::Int) is not defined.

This coding style is widely used. When it is important to handle an error or an exception, this style is inappropriate. Otherwise, it works to catch the problem and allows follow up (albeit somewhat in the weeds).

It’s nice that the interface is relatively straightforward. For completeness, this feels like a pretty good reference for implementing AbstractArrays: Interfaces · The Julia Language, is it also the place to look for knowing what guarantees we have about the behavior of abstract arrays, or perhaps I should look at the docstring of each individual function I call?

1 Like

People wrote that section just for this purpose. You may examine the docstrings of each function as a way to glean deeper understanding – however the point of an interface is to shield us from having to do that.

Yes. Exactly. My intention was to leave the function as is with the knowledge that if someone calls it with a StaticVector they will get the appropriate result: ERROR: setindex!(...) is not defined and it is safe in general. This is in stark contrast to the behavior of for i in 2:length(x); @inbounds x[i] = x[i-1] when passed an OffsetArray. I want to avoid the latter.

1 Like