Do these unit tests cover my method under test? I've written an extension method to truncate strings:

/// &lt;summary&gt;
/// Returns the string, or a substring of the string with a length of &lt;paramref name="maxLength"/&gt; if the string is longer than maxLength. 
/// &lt;/summary&gt;
/// &lt;param name="maxLength"&gt;The maximum length of the string to return.&lt;/param&gt;
/// &lt;exception cref="ArgumentException"&gt;If &lt;paramref name="maxLength"/&gt; is smaller than zero, an ArgumentException is raised.&lt;/exception&gt;
/// &lt;![CDATA[ Documentation:
/// If the string is null or an empty string, the input is returned.
/// If the string is shorter than or equal to maxLength, the input is returned.
/// If the string is longer than maxLength, the first N (where N=maxLength) characters of the string are returned. 
/// ]]&gt;
public static String Truncate(this String input, int maxLength)
{
    if (maxLength &lt; 0)
    {
        throw new ArgumentException("Must be &gt;= 0", "maxLength");
    }

    if (String.IsNullOrEmpty(input) || input.Length &lt;= maxLength)
    {
        return input;
    }

    return input.Substring(0, maxLength);
}


I have written these test cases:

[TestFixture]
public class TruncateTests
{
    String longString = "ABC";
    String shortString = "A";
    String nullOrEmptyString = null;
    String output = "";

    [Test]
    public void LessThanOrEqual()
    {
        // If input is shorter than or equal to maxLength, the input is returned.
        output = longString.Truncate(longString.Length + 5);
        Assert.AreEqual(longString, output);

        output = longString.Truncate(longString.Length);
        Assert.AreEqual(longString, output);
    }

    [Test]
    public void GreaterThan()
    {
        // If input is longer than maxLength, the first N (where N=maxLength) characters of input are returned.
        output = longString.Truncate(1);
        Assert.AreEqual(shortString, output);
    }

    [Test]
    public void NullOrEmpty()
    {
        // If input is null or an empty string, input is returned.
        output = nullOrEmptyString.Truncate(42);
        Assert.AreEqual(output, nullOrEmptyString);

        nullOrEmptyString = "";
        output = nullOrEmptyString.Truncate(42);
        Assert.AreEqual(output, nullOrEmptyString);
    }

    [ExpectedException(typeof(ArgumentException))]
    [Test]
    public void MaxLengthException()
    {
        // If maxLength is smaller than zero, an ArgumentException is raised.
        "".Truncate(-1);
    }

    //http://www.fileformat.info/info/unicode/char/1f3bc/index.htm
    string multiByteString = "\x1F3BC";

    [Test]
    public void MultiByte()
    {
        Assert.IsTrue(multiByteString.Length == 2);

        output = multiByteString.Truncate(1);

        Assert.IsTrue(output.Length == 1);
    }
}


Now how can I confirm that:


The method Truncate() does what it is supposed to do?
I have written test code to test al promises made?
This test code follows practices and guidelines valid for unit testing? 


I'm especially curious about the last one. I've written a few tests in my time, but I'm never sure whether I'm testing enough and not too much at the same time. Can anyone shed a general light on this and maybe point me towards invaluable resources about unit testing?
About the function comment: 

You're over-complicating the summary. As of how I understand the function does Get the first &lt;paramref="maxLength"&gt; characters as a summary. Special conditions are mentioned bellow. Those should be simplified as well. 
For  maxLength I'd check for min/max integer as well as for -1/0/1 (does the function behave correctly at the limits?)
Check input for null and additionally for those combinations:


input.length &lt; maxLength
input.length = maxLength
input.length > maxLength

You're not testing your code when testing for MB-functionality.
Test one thing at a time. If NullOrEmpty fails, how to you know which of those assertions failed? Does it fail if input is empty or if it is null?\
Remove comments, make the test-names more clear instead. Tests are simple and easy to understand (at least they should be :)). Safe your time of writing comments and improve the test instead.  


For most of the functions you're just repeating the behavior asserted. However this is already written down by the assertion itself. The comment just duplicates the code.
In my opinion, test names should be speaking of what they do. NullOrEmpty doesn't state anything about what actually happens. NullInputReturnsNull and EmptyInputReturnsEmptyString do however. Furthermore speaking test-names can be used as a documentation (i.e testdox).  

When are you done? Never. Eventually you (or someone else) will find bugs, misbehavior, ... (e.g. during integration). It's more important to keep the tests up to date at those times as well.


Update

On 2. I'm testing for MAX_INT because your promise is this functions accepts integer values between [0, MAX_INT]. Imagine at some time there is a maxLength + 1 statement for some reason. As a rule of thumb: always test the border values of parameters. Further reading: equivalence partitioning and boundary value analysis

On 3. That'd be four tests. You should split those.

On 4. The point is your are testing the c# library, not your code. You don't have any code dedicated for MB handling. What you are actually doing is testing the Substring method and Length property for MB handling.