./test.sh corre test.sh como um programa separado. Pode acontecer de ser um script bash, se o arquivo test.sh começar com #!/bin/bash . Mas poderia ser algo completamente diferente.
. ./test.sh executa o código do arquivo test.sh dentro da instância em execução do bash. Funciona como se o arquivo de conteúdo test.sh tivesse sido incluído textualmente, em vez da linha . ./test.sh . (Quase: há alguns detalhes que diferem, como o valor de $BASH_LINENO e o comportamento do return incorporado.)
source ./test.sh é idêntico a . ./test.sh no bash (em outros shells, source pode ser ligeiramente diferente ou não existir completamente; . para inclusão está no padrão POSIX).
A diferença mais comumente visível entre executar um script separado com ./test.sh e incluir um script com o . incorporado é que, se o script test.sh definir algumas variáveis de ambiente, com um processo separado, apenas o ambiente do processo filho é definido, enquanto que, com a inclusão de script, o ambiente do processo de shell único é definido. Se você adicionar uma linha foo=bar em test.sh e echo $foo no final do script de chamada, verá a diferença:
$ cat test.sh
#!/bin/sh
foo=bar
$ ./test.sh
$ echo $foo
$ . ./test.sh
$ echo $foo
bar