Removing ddoc and unittest

bearophile bearophileHUGS at lycos.com
Mon Nov 10 05:13:08 PST 2008


Walter Bright:

>Experience has led me to believe that unit tests are extremely valuable, but I rarely see them used - even by professionals.<

In languages with dynamic typing (Python, Ruby, etc) they are used quite often, partially to replace errors that the static typing catches, and partially for other purposes (but after writing about 110.000 lines of D code I have seen that unit tests result very useful in D code too).
In dynamic languages they have even invented a programming style (TDD, Test-Driven Development) that is strictly based on unit tests: you write a unit test first, see it fail, you fix the code to make it pass, you add another unit test, you add a little more code, you see it fail, etc etc. I know it sounds a little crazy, but if you use dynamic languages to write certain classes of programs (surely it's not fit for every kind of code) it seem to work well enough (for some kinds of programmers, I presume).


>I wanted to make them so easy to use in D that it would hook people in. That's why they are the way they are - super simple, next to nothing to learn, and they work.<

I understand. The profilers you are talking about push too much complexity to the final user. But ergonomics shows there are other possible designs for the interfaces of tools: sometimes you can push some more complexity into the product even if its interface is kept simple enough, making it flexible only where it more counts. So I think there are "few things" that can be added to the current unit test system that can increase its usefulness and make it more handy while keeping a simple user interface.

It's not easy to find and list such few things, I can try list something:

1) I'd like a way to state that an expression throws one or more specified exception(s), at runtime, for example:
Throws!(ArgumentException)(foo("hello", -5));
It also has to print that line number of the caller.
I have created something similar, but it's quite less nice:
assert( Throws!(ArgumentException)(foo("hello", -5)) );
See my Throws!() here:
http://www.fantascienza.net/leonardo/so/dlibs/func.html

2) The same at compile time. I think it's impossible to do currently:
static Throws!(AssertError)(foo(5, -5));

3) I need ways to unittest a specified module only. And I'd like to be able to do it even if the main is missing. Having a compiler-managed "mainmodule" boolean constant that is true only in the main module may help.

4) I'd like to unittest nested functions too.

5) Few reflective capabilities can be added to D to help the handy creation of an external unittest system, for the people that need something quite more refined and complex.

--------------------------

I have already given two times links to the wonderful doctest system of Python, but it seems no one has read it, I have seen no one comment on it. So I try a third time, this time I explain a little more.

Note that doctests are unfit for the current D language, but if D gains some runtime capabilities (like I have seen shown here two times), then its phylosophy may become usable.

Note that I am not talking about Test-Driven Development here, this is "normal" way of coding.


This is a little useful Python function that returns true if the given iterable contains items that are all equal. If given an optional mapping function is used to transform items before comparing them:

def allequal(iterable, key=None):
    """allequal(iterable, key=None): return True if all the items of iterable
    are equal. If key is specified it returns True if all the key(item) are equal.
    """
    iseq = iter(iterable)
    try:
        first = iseq.next()
    except StopIteration:
        return True

    if key is None:
        for el in iseq:
            if el != first:
                return False
    else:
        key_first = key(first)
        for el in iseq:
            if key(el) != key_first:
                return False

    return True
    

My D1 version of the same function (you can find it in the "func" module of my dlibs):


/*********************************************
Return true if all the items of the iterable 'items' are equal.
If 'items' is empty return true. If the optional 'key' callable is
specified, it returns true if all the key(item) are equal.
If 'items' is an AA, scans its keys.
*/
bool allEqual(TyIter, TyKey=void*)(TyIter items, TyKey key=null) {
    static if (!IsCallable!(TyKey))
        if (key !is null)
            throw new ArgumentException("allEqual(): key must "
                                        "be a callable or null.");

    bool isFirst = true;

    static if (!is( TyIter == void[0] )) {
        static if (IsCallable!(TyKey)) {
            ReturnType!(TyKey) keyFirstItem;

            static if (IsAA!(TyIter)) {
                foreach (el, _; items)
                    if (isFirst) {
                        isFirst = false;
                        keyFirstItem = key(el);
                    } else {
                        if (key(el) != keyFirstItem)
                            return false;
                    }
            } else static if (IsArray!(TyIter)) {
                if (items.length > 1) {
                    keyFirstItem = key(items[0]);
                    foreach (el; items[1 .. $])
                        if (key(el) != keyFirstItem)
                            return false;
                }
            } else {
                foreach (el; items)
                    if (isFirst) {
                        isFirst = false;
                        keyFirstItem = key(el);
                    } else {
                        if (key(el) != keyFirstItem)
                            return false;
                    }
            }
        } else {
            BaseType1!(TyIter) firstItem;

            static if (IsAA!(TyIter)) {
                return items.length < 2 ? true : false;
            } else static if (IsArray!(TyIter)) {
                if (items.length > 1) {
                    firstItem = items[0];
                    foreach (el; items[1 .. $])
                        if (el != firstItem)
                            return false;
                }
            } else {
                foreach (el; items)
                    if (isFirst) {
                        isFirst = false;
                        firstItem = el;
                    } else {
                        if (el != firstItem)
                            return false;
                    }
            }
        }
    }

    return true;
} // end allEqual()


Its unit tests:

unittest { // Tests of allEqual()
    // array
    assert(allEqual([]));
    assert(allEqual(new int[0]));
    assert(allEqual([1]));
    assert(!allEqual([1, 1, 2]));
    assert(allEqual([1, 1, 1]));
    assert(allEqual("aaa"));
    assert(!allEqual("aab"));

    // array with key function
    int abs(int x) { return x >= 0 ? x : -x; }
    assert(allEqual([], &abs));
    assert(allEqual(new int[0], &abs));
    assert(allEqual([1], &abs));
    assert(allEqual([1, -1], &abs));
    assert(!allEqual([1, -2], &abs));

    // AA
    assert(allEqual(AA!(int, int)));
    assert(allEqual([1: 1]));
    assert(!allEqual([1: 1, 2: 2]));
    assert(!allEqual([1: 1, 2: 2, 3: 3]));

    // AA with key function
    assert(allEqual(AA!(int, int)));
    assert(allEqual([1: 1], &abs));
    assert(!allEqual([1: 1, 2: 2], &abs));
    assert(allEqual([1: 1, -1: 2], &abs));
    assert(!allEqual([1: 1, -1: 2, 2: 3], &abs));

    // with an iterable
    struct IterInt { // iterable wrapper
        int[] items;
        int opApply(int delegate(ref int) dg) {
            int result;
            foreach (el; this.items) {
                result = dg(el);
                if (result) break;
            }
            return result;
        }
    }

    assert(allEqual(IterInt(new int[0])));
    assert(allEqual(IterInt([1])));
    assert(!allEqual(IterInt([1, 1, 2])));
    assert(allEqual(IterInt([1, 1, 1])));

    // iterable with key function
    assert(allEqual(IterInt(new int[0]), &abs));
    assert(allEqual(IterInt([1]), &abs));
    assert(allEqual(IterInt([1, -1]), &abs));
    assert(!allEqual(IterInt([1, -2]), &abs));
} // End tests of allEqual()


In Python if I want to use doctests I can start the Python shell, import a module that contains that allequal() function, and try it in various ways. If I find bugs or strange outputs I can also debug it, etc. Let's say there are no bugs, then this can be the log of the usage of allequal() in that shell:


>>> from util import allequal
>>> allequal()
Traceback (most recent call last):
  ...
TypeError: allequal() takes at least 1 argument (0 given)
>>> allequal([])
True
>>> allequal([1])
True
>>> allequal([1, 1L, 1.0, 1.0+0.0J])
True
>>> allequal([1, 1, 2])
False
>>> allequal([1, -1, -1.0], key=abs)
True
>>> allequal(iter([]))
True
>>> allequal(iter([]), key=abs)
True
>>> allequal(iter([1]), key=abs)
True
>>> allequal(iter([1, 2]), key=abs)
False


Then I can just copy and paste that log into the docstring of the function (the docstring is similar to the /** ... */ or /// of D):


def allequal(iterable, key=None):
    """allequal(iterable, key=None): return True if all the items of iterable are equal.
    If key is specified it returns True if all the key(item) are equal.

    >>> allequal()
    Traceback (most recent call last):
      ...
    TypeError: allequal() takes at least 1 argument (0 given)
    >>> allequal([])
    True
    >>> allequal([1])
    True
    >>> allequal([1, 1L, 1.0, 1.0+0.0J])
    True
    >>> allequal([1, 1, 2])
    False
    >>> allequal([1, -1, -1.0], key=abs)
    True
    >>> allequal(iter([]))
    True
    >>> allequal(iter([]), key=abs)
    True
    >>> allequal(iter([1]), key=abs)
    True
    >>> allequal(iter([1, 2]), key=abs)
    False
    """
    iseq = iter(iterable)
    try:
        first = iseq.next()
    except StopIteration:
        return True

    if key is None:
        for el in iseq:
            if el != first:
                return False
    else:
        key_first = key(first)
        for el in iseq:
            if key(el) != key_first:
                return False

    return True


At the end of the module where allequal() is I just need to add:

if __name__ == "__main__":
    import doctest
    doctest.testmod()


(Where if __name__==... is true only in the main module).
Note that Traceback, it's a test failed on purpose.
Now that shell log is run and the resuls of each expression is compared to the given results. If they are different, then a test is failed, and at the end a list of the failed ones is shown.
If the module is imported, the tests aren't run.

doctests can't be used for everything, there are more complex and refined ways to test in Python, but for quicker/smaller purposes it's godsend. I've never found something as handy to write tests in 20+ other languages.

Bye,
bearophile



More information about the Digitalmars-d mailing list