Q:

Why do I need to quote variable for if, but not for echo?

I've read that you need double quotes for expanding variables, e.g.
if [ -n "$test" ]; then echo '$test ok'; else echo '$test null'; fi

will work as expected, while
if [ -n $test ]; then echo '$test ok'; else echo '$test null'; fi

will always say $test ok even if $test is null.
but then why don't we need quotes in echo $test?

A:

You always need quotes around variables in all list contexts, that is everywhere the variable may be expanded to multiple values unless you do want the 3 side effects of leaving a variable unquoted.
list contexts include arguments to simple commands like [ or echo, the for i in <here>, assignments to arrays... There are other contexts where variables also need to be quoted. Best is to always quote variables unless you've got a very good reason not to.
Think of the absence of quotes (in list contexts) as the split+glob operator.
As if echo $test was echo glob(split("$test")).
The shell behaviour is confusing to most people because in most other languages, you put quotes around fixed strings, like puts("foo"), and not around variables (like puts(var)) while in shell it's the other way round: everything is string in shell, so putting quotes around everything would be cumbersome, you echo test, you don't need to "echo" "test". In shell, quotes are used for something else: prevent some special meaning of some characters and/or affect the behaviour of some expansions.
In [ -n $test ] or echo $test, the shell will split $test (on blanks by default), and then perform filename generation (expand all the *, '?'... patterns to the list of matching files), and then pass that list of arguments to the [ or echo commands.
Again, think of it as "[" "-n" glob(split("$test")) "]". If $test is empty or contains only blanks (spc, tab, nl), then the split+glob operator will return an empty list, so the [ -n $test ] will be "[" "-n" "]", which is a test to check wheter "-n" is the empty string or not. But imagine what would have happened if $test was "*" or "= foo"...
In [ -n "$test" ], [ is passed the four arguments "[", "-n", "" and "]" (without the quotes), which is what we want.
Whether it's echo or [ makes no difference, it's just that echo outputs the same thing whether it's passed an empty argument or no argument at all.
See also this answer to a similar question for more details on the [ command and the [[...]] construct.

A:

@h3rrmiller's answer is good for explaining why you need the quotes for the if (or rather, [/test), but I would actually posit that your question is incorrect.
Try the following commands, and you will see what I mean.
export testvar="123    456"
echo $testvar
echo "$testvar"

Without the quotes, the variable substitution causes the second command to expand to:
echo 123    456

and the multiple spaces are collapsed to a single one:
echo 123 456

With the quotes, the spaces are preserved.
This happens because when you quote a parameter (whether that parameter is passed to echo, test or some other command), the value of that parameter is sent as one value to the command. If you don't quote it, the shell does its normal magic of looking for whitespace to determine where each parameter starts and ends.
This can also be illustrated by the following (very very simple) C program. Try the following on the command line (you may want to do it in an empty directory so as to not risking to overwrite something).
cat <<EOF >paramtest.c
#include <stdio.h>
int main(int argc, char **argv) {
  int nparams = argc-1; /* because 1 parameter means only the executable's name */
  printf("%d parameters received\n", nparams);
  return nparams;
}
EOF
cc -o paramtest paramtest.c

and then...
./paramtest 123 456
./paramtest "123 456"
./paramtest 123   456
./paramtest "123   456"

After running paramtest, $? will hold the number of parameters it was passed (and that number will be printed).

