The e-TeX reference defines \scantokens as follows:
\scantokens, when followed by a <general text>, decomposes the <balanced text> of the <general text> into the corresponding sequence of characters as if the <balanced text> were written unexpanded to a file; it then uses TeX's \input mechanism to re-process these characters under the current \catcode regime.
So the \def! part of your macro is processed as if it were defined in an extra file that is then \input into your main file. This includes the use of an internal end-fo-file marker when the whole <balanced text> has been processed.
In The TeXbook, p. 206, Knuth makes a note on file reading and \outer macros (thanks to GuM for pointing this out):
An \outer macro cannot appear in an argument (not even when \par is allowed), nor can it appear in the parameter text or the replacement text of a definition [...] The end of an input file or alignment template is also considered to be \outer in this sense; for example, a file shouldn't end in the middle of a definition.
This is exactly the problem you run into here. TeX is still scanning the definition of ! when the end of file after \scantokens occurs. It then complains about File ended while scanning definition of !. You can see the same behavior and error message if you replace \scantokens{\def!} by an actual \input were the input file includes only \def!.
Bruno Le Floch found a nice way to get around this problem by using a \let instead of \def to define !:
\def\bangdef{test} \def\test{\catcode`!=\active \scantokens{\let!}\bangdef} \test
For completeness, here are two standard tricks that do not make use of \scantokens. One way to get the desired result is by defining \test in a regime which has the correct catcode already applied:
\begingroup \catcode`!=\active \gdef\test{\catcode`!=\active \def!{test}} \endgroup \test \show!
Instead of the \gdef you could also save the current catcode of ! and restore it after the definition of \test.
Another common trick is to change the lowercase code of an active character, here ~, to the character code of the character to be made active, and then use \lowercase to apply this change:
\def\test{% \begingroup \lccode`\~=`\! \lowercase{\endgroup \catcode`\!=\active \def~}{test}% } \test \show!